Stilul global vs. local în Next.js

Publicat: 2022-03-10
Rezumat rapid ↬ Next.js are opinii puternice despre cum să organizați JavaScript, dar nu CSS. Cum putem dezvolta modele care încurajează cele mai bune practici CSS, respectând și logica cadrului? Răspunsul este surprinzător de simplu - să scrieți CSS bine structurat, care echilibrează preocupările de stil globale și locale.

Am avut o experiență grozavă folosind Next.js pentru a gestiona proiecte front-end complexe. Next.js are păreri despre cum să organizezi codul JavaScript, dar nu are păreri încorporate despre cum să organizezi CSS.

După ce am lucrat în cadrul acestui cadru, am găsit o serie de modele organizaționale care cred că se conformează atât filozofiilor directoare ale Next.js, cât și exercită cele mai bune practici CSS. În acest articol, vom construi împreună un site web (o ceainărie!) pentru a demonstra aceste modele.

Notă : probabil că nu veți avea nevoie de experiență anterioară în Next.js, deși ar fi bine să aveți o înțelegere de bază a React și să fiți deschis pentru a învăța câteva tehnici CSS noi.

Scrierea CSS „de modă veche”.

Când ne uităm pentru prima dată în Next.js, putem fi tentați să luăm în considerare utilizarea unui fel de bibliotecă CSS-in-JS. Deși pot exista beneficii în funcție de proiect, CSS-in-JS introduce multe considerații tehnice. Necesită utilizarea unei noi biblioteci externe, care se adaugă la dimensiunea pachetului. CSS-in-JS poate avea, de asemenea, un impact asupra performanței, provocând randări suplimentare și dependențe de starea globală.

Lectură recomandată : „ Costurile de performanță nevăzute ale bibliotecilor moderne CSS-in-JS în aplicațiile React)” de Aggelos Arvanitakis

În plus, scopul utilizării unei biblioteci precum Next.js este de a reda static activele ori de câte ori este posibil, așa că nu are atât de mult sens să scrieți JS care trebuie să fie rulat în browser pentru a genera CSS.

Există câteva întrebări pe care trebuie să le luăm în considerare atunci când organizăm stilul în Next.js:

Cum ne putem încadra în convențiile/cele mai bune practici ale cadrului?

Cum putem echilibra preocupările de stil „global” (fonturi, culori, machete principale și așa mai departe) cu cele „locale” (stiluri privind componentele individuale)?

Răspunsul cu care am venit la prima întrebare este să scriu pur și simplu CSS de modă veche . Nu numai că Next.js acceptă acest lucru fără setări suplimentare; de asemenea, dă rezultate performante și statice.

Pentru a rezolva a doua problemă, am o abordare care poate fi rezumată în patru bucăți:

  1. Jetoane de proiectare
  2. Stiluri globale
  3. Clasele de utilitate
  4. Stiluri de componente

Sunt îndatorat ideii lui Andy Bell despre CUBE CSS („Compoziție, utilitate, blocare, excepție”) aici. Dacă nu ați auzit până acum de acest principiu organizatoric, v-am recomandat să consultați site-ul său oficial sau caracteristica de pe Smashing Podcast. Unul dintre principiile pe care le vom lua de la CUBE CSS este ideea că ar trebui să îmbrățișăm mai degrabă decât să ne temem de cascada CSS. Să învățăm aceste tehnici aplicându-le unui proiect de site web.

Noțiuni de bază

Vom construi un magazin de ceai pentru că, ei bine, ceaiul este gustos. Vom începe prin a rula yarn create next-app pentru a crea un nou proiect Next.js. Apoi, vom elimina tot ce se află în styles/ directory (totul este cod exemplu).

Notă : Dacă doriți să urmăriți proiectul finalizat, îl puteți verifica aici.

Jetoane de proiectare

În aproape orice configurare CSS, există un avantaj clar în stocarea tuturor valorilor partajate la nivel global în variabile . Dacă un client cere schimbarea unei culori, implementarea schimbării este o singură linie, mai degrabă decât o mizerie masivă de găsire și înlocuire. În consecință, o parte cheie a configurației noastre CSS Next.js va fi stocarea tuturor valorilor la nivel de site ca simboluri de proiectare .

Vom folosi proprietăți personalizate CSS încorporate pentru a stoca aceste simboluri. (Dacă nu sunteți familiarizat cu această sintaxă, puteți consulta „A Strategy Guide To CSS Custom Properties”.) Ar trebui să menționez că (în unele proiecte) am optat să folosesc variabile SASS/SCSS în acest scop. Nu am găsit niciun avantaj real, așa că de obicei includ SASS într-un proiect doar dacă găsesc că am nevoie de alte funcții SASS (mix-in-uri, iterație, import de fișiere și așa mai departe). Proprietățile personalizate CSS, prin contrast, funcționează și cu cascada și pot fi modificate în timp, mai degrabă decât compilarea static. Deci, pentru astăzi, să rămânem cu CSS simplu .

În directorul nostru styles/ , să creăm un nou fișier design_tokens.css :

 :root { --green: #3FE79E; --dark: #0F0235; --off-white: #F5F5F3; --space-sm: 0.5rem; --space-md: 1rem; --space-lg: 1.5rem; --font-size-sm: 0.5rem; --font-size-md: 1rem; --font-size-lg: 2rem; }

Desigur, această listă poate și va crește în timp. Odată ce adăugăm acest fișier, trebuie să trecem la fișierul nostru pages/_app.jsx , care este aspectul principal pentru toate paginile noastre, și să adăugăm:

 import '../styles/design_tokens.css'

Îmi place să mă gândesc la jetoanele de design ca fiind lipiciul care menține consistența în întregul proiect. Vom face referire la aceste variabile la scară globală, precum și în cadrul componentelor individuale, asigurând un limbaj de design unificat.

Mai multe după săritură! Continuați să citiți mai jos ↓

Stiluri globale

În continuare, să adăugăm o pagină pe site-ul nostru! Să intrăm în fișierul pages/index.jsx (aceasta este pagina noastră de pornire). Vom șterge toate boilerplate și vom adăuga ceva de genul:

 export default function Home() { return <main> <h1>Soothing Teas</h1> <p>Welcome to our wonderful tea shop.</p> <p>We have been open since 1987 and serve customers with hand-picked oolong teas.</p> </main> }

Din păcate, va arăta destul de simplu, așa că haideți să setăm câteva stiluri globale pentru elementele de bază , de exemplu etichetele <h1> . (Îmi place să cred că aceste stiluri sunt „valori prestabilite globale rezonabile”.) S-ar putea să le înlocuim în cazuri specifice, dar sunt o bună presupunere a ceea ce ne vom dori dacă nu o facem.

Voi pune asta în fișierul styles/globals.css (care vine implicit de la Next.js):

 *, *::before, *::after { box-sizing: border-box; } body { color: var(--off-white); background-color: var(--dark); } h1 { color: var(--green); font-size: var(--font-size-lg); } p { font-size: var(--font-size-md); } p, article, section { line-height: 1.5; } :focus { outline: 0.15rem dashed var(--off-white); outline-offset: 0.25rem; } main:focus { outline: none; } img { max-width: 100%; }

Desigur, această versiune este destul de simplă, dar fișierul meu globals.css nu ajunge, de obicei, să fie prea mare. Aici, stil elementele HTML de bază (titluri, corp, linkuri și așa mai departe). Nu este nevoie să includeți aceste elemente în componente React sau să adăugați în mod constant clase doar pentru a oferi un stil de bază.

De asemenea, includ orice resetări ale stilurilor implicite de browser . Ocazional, voi avea un stil de aspect la nivelul întregului site pentru a oferi un „subsol lipicios”, de exemplu, dar ele aparțin aici doar dacă toate paginile au același aspect. În caz contrar, va trebui să fie analizat în interiorul componentelor individuale.

Includ întotdeauna un fel de stil :focus pentru a indica clar elementele interactive pentru utilizatorii de tastatură atunci când sunt concentrați. Cel mai bine este să îl faceți parte integrantă din ADN-ul de design al site-ului!

Acum, site-ul nostru începe să se contureze:

Imagine a site-ului web în lucru. Fundalul paginii este acum de culoare albastru închis, iar titlul „Ceaiuri liniștitoare” este verde. Site-ul web nu are aspect/spațiere și astfel se extinde pe lățimea ferestrei browserului complet.
Imagine a site-ului web în lucru. Fundalul paginii este acum de culoare albastru închis, iar titlul „Ceaiuri liniștitoare” este verde. Site-ul web nu are aspect/spațiere și astfel se extinde pe lățimea ferestrei browserului complet. (Previzualizare mare)

Clasele de utilitate

Un domeniu în care pagina noastră de pornire s-ar putea îmbunătăți cu siguranță este că textul în prezent se extinde întotdeauna pe părțile laterale ale ecranului, așa că haideți să-i limităm lățimea. Avem nevoie de acest aspect pe această pagină, dar îmi imaginez că ar putea avea nevoie de el și în alte pagini. Acesta este un caz de utilizare grozav pentru o clasă de utilitate!

Încerc să folosesc clasele de utilitate mai degrabă decât ca un înlocuitor pentru doar scrierea CSS. Criteriile mele personale pentru momentul în care are sens să adaug unul la un proiect sunt:

  1. Am nevoie de el în mod repetat;
  2. Face un lucru bine;
  3. Se aplică într-o serie de componente sau pagini diferite.

Cred că acest caz îndeplinește toate cele trei criterii, așa că haideți să facem un nou fișier CSS styles/utilities.css și să adăugăm:

 .lockup { max-width: 90ch; margin: 0 auto; }

Apoi, să adăugăm importul '../styles/utilities.css' în paginile noastre/_app.jsx . În cele din urmă, să schimbăm eticheta <main> din paginile noastre/index.jsx în <main className="lockup"> .

Acum, pagina noastră se adună și mai mult. Deoarece am folosit proprietatea max-width , nu avem nevoie de interogări media pentru a face aspectul nostru mobil receptiv. Și, pentru că am folosit unitatea de măsură ch - care echivalează cu aproximativ lățimea unui caracter - dimensiunea noastră este dinamică în funcție de dimensiunea fontului browserului utilizatorului.

Același site web ca înainte, dar acum textul este prins în mijloc și nu devine prea lat
Același site web ca înainte, dar acum textul este prins în mijloc și nu devine prea lat. (Previzualizare mare)

Pe măsură ce site-ul nostru web crește, putem continua să adăugăm mai multe clase de utilitate. Eu adopt aici o abordare destul de utilitarista: daca lucrez si gasesc ca am nevoie de o alta clasa pentru o culoare sau ceva, o adaug. Nu adaug toate clasele posibile sub soare - ar mări dimensiunea fișierului CSS și ar face codul meu confuz. Uneori, în proiecte mai mari, îmi place să despart lucrurile într-un director de styles/utilities/ cu câteva fișiere diferite; depinde de nevoile proiectului.

Ne putem gândi la clasele de utilitate ca setul nostru de instrumente de comenzi de stil obișnuite, repetate, care sunt partajate la nivel global. Ele ne ajută să ne împiedicăm să rescriem în mod constant același CSS între diferite componente.

Stiluri componente

Am terminat pagina noastră de pornire pentru moment, dar mai trebuie să construim o parte din site-ul nostru: magazinul online. Scopul nostru aici va fi să afișăm o grilă de card cu toate ceaiurile pe care dorim să le vindem , așa că va trebui să adăugăm câteva componente pe site-ul nostru.

Să începem prin adăugarea unei noi pagini la pages/shop.jsx :

 export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }

Apoi, vom avea nevoie de niște ceaiuri de afișat. Vom include un nume, o descriere și o imagine (în directorul public/) pentru fiecare ceai:

 const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]

Notă : Acesta nu este un articol despre preluarea datelor, așa că am luat calea ușoară și am definit o matrice la începutul fișierului.

În continuare, va trebui să definim o componentă pentru a ne afișa ceaiurile. Să începem prin a face un director de components/ (Next.js nu face acest lucru în mod implicit). Apoi, să adăugăm un director de components/TeaList . Pentru orice componentă care ajunge să aibă nevoie de mai mult de un fișier, de obicei pun toate fișierele aferente într-un folder. Procedând astfel, se împiedică components/ dosarul nostru să devină imposibil de navigat.

Acum, să adăugăm fișierul componentele/TeaList/TeaList.jsx :

 import TeaListItem from './TeaListItem' const TeaList = (props) => { const { teas } = props return <ul role="list"> {teas.map(tea => <TeaListItem tea={tea} key={tea.name} />)} </ul> } export default TeaList

Scopul acestei componente este de a repeta peste ceaiurile noastre și de a afișa un articol de listă pentru fiecare, așa că acum să definim componentele noastre/TeaList/TeaListItem.jsx :

 import Image from 'next/image' const TeaListItem = (props) => { const { tea } = props return <li> <div> <Image src={tea.image} alt="" objectFit="cover" objectPosition="center" layout="fill" /> </div> <div> <h2>{tea.name}</h2> <p>{tea.description}</p> </div> </li> } export default TeaListItem

Rețineți că folosim componenta de imagine încorporată a Next.js. Am setat atributul alt la un șir gol, deoarece imaginile sunt pur decorative în acest caz; vrem să evităm blocarea utilizatorilor cititorilor de ecran cu descrieri lungi de imagini aici.

În cele din urmă, să creăm un fișier components/TeaList/index.js , astfel încât componentele noastre să fie ușor de importat extern:

 import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList

Și apoi, să le conectăm pe toate adăugând import TeaList din ../components/TeaList și un <TeaList teas={teas} /> la pagina noastră Magazin. Acum, ceaiurile noastre vor apărea într-o listă, dar nu va fi atât de drăguță.

Colocarea stilului cu componente prin modulele CSS

Să începem prin a ne modela cardurile (componenta TeaListLitem ). Acum, pentru prima dată în proiectul nostru, vom dori să adăugăm un stil care este specific unei singure componente. Să creăm un fișier nou componente/TeaList/TeaListItem.module.css .

S-ar putea să vă întrebați despre modulul din extensia de fișier. Acesta este un modul CSS . Next.js acceptă module CSS și include o documentație bună despre acestea. Când scriem un nume de clasă dintr-un modul CSS, cum ar fi .TeaListItem , acesta va fi automat transformat în ceva mai degrabă . TeaListItem_TeaListItem__TFOk_ . TeaListItem_TeaListItem__TFOk_ cu o grămadă de caractere suplimentare lipite. În consecință, putem folosi orice nume de clasă dorim fără a ne îngrijora că va intra în conflict cu alte nume de clasă în altă parte a site-ului nostru.

Un alt avantaj al modulelor CSS este performanța. Next.js include o funcție de import dinamic. next/dynamic ne permite să încărcăm lene componentele, astfel încât codul lor să fie încărcat numai atunci când este necesar, în loc să adăugăm la întreaga dimensiune a pachetului. Dacă importăm stilurile locale necesare în componente individuale, atunci utilizatorii pot încărca lene CSS-ul pentru componentele importate dinamic . Pentru proiectele mari, putem alege să încărcăm leneș bucăți semnificative din codul nostru și să încărcăm în avans doar cel mai necesar JS/CSS. Ca rezultat, de obicei ajung să fac un nou fișier de modul CSS pentru fiecare componentă nouă care necesită stil local.

Să începem prin a adăuga câteva stiluri inițiale la fișierul nostru:

 .TeaListItem { display: flex; flex-direction: column; gap: var(--space-sm); background-color: var(--color, var(--off-white)); color: var(--dark); border-radius: 3px; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }

Apoi, putem importa stilul din ./TeaListItem.module.css în componenta noastră TeaListitem . Variabila de stil vine ca un obiect JavaScript, astfel încât să putem accesa acest stil de style.TeaListItem.

Notă : Numele clasei noastre nu trebuie să fie scris cu majuscule. Am descoperit că o convenție de nume de clase cu majuscule în interiorul modulelor (și litere mici în exterior) diferențiază vizual numele de clasă locale de cele globale.

Deci, să luăm noua noastră clasă locală și să o atribuim <li> din componenta noastră TeaListItem :

 <li className={style.TeaListComponent}>

S-ar putea să vă întrebați despre linia de culoare de fundal (adică var(--color, var(--off-white)); ). Ceea ce înseamnă acest fragment este că , implicit , fundalul va fi valoarea noastră --off-white . Dar, dacă setăm o proprietate personalizată --color pe un card, aceasta va înlocui și alege acea valoare.

La început, vom dori ca toate cărțile noastre să fie --off-white , dar este posibil să dorim să modificăm valoarea cardurilor individuale mai târziu. Acest lucru funcționează foarte similar cu recuzita din React. Putem seta o valoare implicită, dar creăm un slot în care putem alege alte valori în circumstanțe specifice. Așadar, ne încurajez să ne gândim la proprietăți personalizate CSS, cum ar fi versiunea CSS a props .

Stilul încă nu va arăta grozav, deoarece vrem să ne asigurăm că imaginile rămân în containerele lor. Componenta Imagine a Next.js cu prop layout="fill" primește position: absolute; din cadru, deci putem limita dimensiunea punand intr-un recipient cu pozitia: relativa;.

Să adăugăm o nouă clasă la TeaListItem.module.css .

 .ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }

Și apoi să adăugăm className={styles.ImageContainer} pe <div> care conține <Image> . Folosesc nume relativ „simple”, cum ar fi ImageContainer , deoarece ne aflăm în interiorul unui modul CSS, deci nu trebuie să ne facem griji cu privire la conflictul cu stilul exterior.

În cele din urmă, dorim să adăugăm un pic de umplutură pe părțile laterale ale textului, așa că haideți să adăugăm o ultimă clasă și să ne bazăm pe variabilele de spațiere pe care le-am configurat ca simboluri de design:

 .Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }

Putem adăuga această clasă la <div> care conține numele și descrierea noastră. Acum, cărțile noastre nu arată atât de rău:

Cardurile sunt afișate pentru 3 ceaiuri diferite care au fost adăugate ca date de semințe. Au imagini, nume și descrieri. În prezent, ele apar într-o listă verticală fără spațiu între ele.
Cardurile sunt afișate pentru 3 ceaiuri diferite care au fost adăugate ca date de semințe. Au imagini, nume și descrieri. În prezent, ele apar într-o listă verticală fără spațiu între ele. (Previzualizare mare)

Combinând stilul global și local

Apoi, vrem să avem cărțile noastre să fie afișate într-un aspect de grilă. În acest caz, suntem doar la granița dintre stilurile locale și cele globale. Cu siguranță ne-am putea codifica aspectul direct pe componenta TeaList . Dar, mi-aș putea imagina și că a avea o clasă de utilitate care transformă o listă într-un aspect de grilă ar putea fi util în mai multe alte locuri.

Să luăm o abordare globală aici și să adăugăm o nouă clasă de utilitate în styles/utilities.css :

 .grid { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--min-item-width, 30ch), 1fr)); gap: var(--space-md); }

Acum, putem adăuga clasa .grid pe orice listă și vom obține un aspect al grilei care răspunde automat. De asemenea, putem modifica proprietatea personalizată --min-item-width (în mod implicit 30ch ) pentru a modifica lățimea minimă a fiecărui element.

Notă : Amintiți-vă să vă gândiți la proprietăți personalizate, cum ar fi recuzita! Dacă această sintaxă pare necunoscută, puteți consulta „Grilă CSS cu răspuns intrinsec cu minmax() și min() ” de Chris Coyier.

Deoarece am scris acest stil la nivel global, nu necesită nicio fantezie pentru a adăuga className="grid" pe componenta noastră TeaList . Dar, să presupunem că vrem să cuplăm acest stil global cu un magazin local suplimentar. De exemplu, vrem să aducem un pic mai mult din „estetica ceaiului” și să facem ca orice altă carte să aibă un fundal verde. Tot ce trebuie să facem este să creăm un fișier nou componente/TeaList/TeaList.module.css :

 .TeaList > :nth-child(even) { --color: var(--green); }

Vă amintiți cum am creat o proprietate --color custom pe componenta noastră TeaListItem ? Ei bine, acum îl putem seta în circumstanțe specifice. Rețineți că putem folosi în continuare selectoare copii în modulele CSS și nu contează că selectăm un element care este stilat în interiorul unui modul diferit. Deci, putem folosi și stilurile noastre de componente locale pentru a afecta componentele secundare. Aceasta este mai degrabă o caracteristică decât o eroare, deoarece ne permite să profităm de cascada CSS ! Dacă am încerca să reproducem acest efect într-un alt mod, probabil că am ajunge cu un fel de supă JavaScript, mai degrabă decât trei linii de CSS.

Apoi, cum putem păstra clasa globală .grid pe componenta noastră TeaList , adăugând și clasa locală .TeaList ? Aici sintaxa poate deveni un pic ciudată, deoarece trebuie să accesăm clasa noastră .TeaList din modulul CSS făcând ceva de genul style.TeaList .

O opțiune ar fi să folosiți interpolarea șirurilor pentru a obține ceva de genul:

 <ul role="list" className={`${style.TeaList} grid`}>

În acest caz mic, acest lucru ar putea fi suficient de bun. Dacă amestecăm și potrivim mai multe clase, constat că această sintaxă îmi face creierul să explodeze puțin, așa că voi opta uneori să folosesc biblioteca de nume de clasă. În acest caz, ajungem cu o listă mai sensibilă:

 <ul role="list" className={classnames(style.TeaList, "grid")}>

Acum, am terminat pagina noastră de magazin și am făcut ca componenta noastră TeaList să profite atât de stilul global, cât și de cel local.

Cardurile noastre de ceai sunt acum afișate într-o grilă. Întregulile pare sunt colorate în verde, în timp ce intrările impare sunt albe.
Cardurile noastre de ceai sunt acum afișate într-o grilă. Întregulile pare sunt colorate în verde, în timp ce intrările impare sunt albe. (Previzualizare mare)

Un act de echilibrare

Acum ne-am construit ceainăria folosind doar CSS simplu pentru a gestiona stilul. Poate că ați observat că nu a trebuit să petrecem o mulțime de ani ocupându-ne de setări personalizate Webpack, instalând biblioteci externe și așa mai departe. Acest lucru se datorează modelelor pe care le-am folosit pentru a lucra cu Next.js. În plus, încurajează cele mai bune practici CSS și se încadrează în mod natural în arhitectura cadru Next.js.

Organizația noastră CSS a constat din patru piese cheie:

  1. Jetoane de proiectare,
  2. Stiluri globale,
  3. clase de utilitate,
  4. Stiluri de componente.

Pe măsură ce continuăm să ne construim site-ul, lista noastră de jetoane de design și clase de utilitate va crește. Orice stil care nu are sens să fie adăugat ca clasă de utilitate, îl putem adăuga în stilurile componente folosind module CSS. Ca rezultat, putem găsi un echilibru continuu între preocupările de stil local și globale. De asemenea, putem genera cod CSS performant, intuitiv, care crește în mod natural alături de site-ul nostru Next.js.