Globales vs. lokales Styling in Next.js
Veröffentlicht: 2022-03-10Ich habe großartige Erfahrungen mit Next.js gemacht, um komplexe Front-End-Projekte zu verwalten. Next.js hat eine eigene Meinung darüber, wie man JavaScript-Code organisiert, aber es hat keine eingebauten Meinungen darüber, wie man CSS organisiert.
Nachdem ich innerhalb des Frameworks gearbeitet habe, habe ich eine Reihe von Organisationsmustern gefunden, von denen ich glaube, dass sie sowohl den Leitphilosophien von Next.js entsprechen als auch die besten CSS-Praktiken anwenden. In diesem Artikel werden wir gemeinsam eine Website (einen Teeladen!) erstellen, um diese Muster zu demonstrieren.
Hinweis : Sie benötigen wahrscheinlich keine Erfahrung mit Next.js, obwohl es gut wäre, ein grundlegendes Verständnis von React zu haben und bereit zu sein, einige neue CSS-Techniken zu lernen.
„Altmodisches“ CSS schreiben
Wenn wir uns zum ersten Mal mit Next.js befassen, könnten wir versucht sein, eine Art CSS-in-JS-Bibliothek zu verwenden. Obwohl es je nach Projekt Vorteile geben kann, führt CSS-in-JS viele technische Überlegungen ein. Es erfordert die Verwendung einer neuen externen Bibliothek, die die Paketgröße erhöht. CSS-in-JS kann sich auch auf die Leistung auswirken, indem es zusätzliche Renderings und Abhängigkeiten vom globalen Status verursacht.
Empfohlene Lektüre : „ The Unseen Performance Costs of Modern CSS-in-JS Libraries In React Apps)“ von Aggelos Arvanitakis
Darüber hinaus besteht der springende Punkt bei der Verwendung einer Bibliothek wie Next.js darin, Assets wann immer möglich statisch zu rendern, sodass es nicht so sinnvoll ist, JS zu schreiben, das im Browser ausgeführt werden muss, um CSS zu generieren.
Es gibt ein paar Fragen, die wir berücksichtigen müssen, wenn wir den Stil in Next.js organisieren:
Wie können wir uns an die Konventionen/Best Practices des Frameworks anpassen?
Wie können wir „globale“ Styling-Belange (Schriftarten, Farben, Hauptlayouts usw.) mit „lokalen“ (Stile in Bezug auf einzelne Komponenten) in Einklang bringen?
Die Antwort, die ich auf die erste Frage gefunden habe, ist, einfach gutes altmodisches CSS zu schreiben . Next.js unterstützt dies nicht nur ohne zusätzliche Einrichtung; es liefert auch Ergebnisse, die performant und statisch sind.
Um das zweite Problem zu lösen, wähle ich einen Ansatz, der sich in vier Teile zusammenfassen lässt:
- Design-Token
- Globale Stile
- Gebrauchsklassen
- Komponentenstile
Ich bin hier Andy Bells Idee von CUBE CSS („Composition, Utility, Block, Exception“) zu Dank verpflichtet. Wenn Sie noch nie von diesem Organisationsprinzip gehört haben, empfehle ich Ihnen, sich die offizielle Website oder das Feature im Smashing Podcast anzusehen. Eines der Prinzipien, die wir von CUBE CSS übernehmen werden, ist die Idee, dass wir die CSS-Kaskade annehmen sollten, anstatt sie zu fürchten. Lassen Sie uns diese Techniken lernen, indem wir sie auf ein Website-Projekt anwenden.
Einstieg
Wir werden einen Teeladen bauen, weil Tee lecker ist. Wir beginnen mit der Ausführung von „ yarn create next-app
“, um ein neues Next.js-Projekt zu erstellen. Dann entfernen wir alles im styles/ directory
(es ist alles Beispielcode).
Hinweis : Wenn Sie das fertige Projekt verfolgen möchten, können Sie es sich hier ansehen.
Design-Token
In so ziemlich jedem CSS-Setup hat es einen klaren Vorteil, alle global freigegebenen Werte in variables zu speichern . Wenn ein Kunde darum bittet, eine Farbe zu ändern, ist die Implementierung der Änderung eher ein Einzeiler als ein massives Suchen-und-Ersetzen-Durcheinander. Folglich wird ein wichtiger Teil unseres Next.js-CSS-Setups darin bestehen, alle Website-weiten Werte als Design-Token zu speichern.
Wir verwenden integrierte benutzerdefinierte CSS-Eigenschaften, um diese Token zu speichern. (Wenn Sie mit dieser Syntax nicht vertraut sind, können Sie „Ein Strategieleitfaden für benutzerdefinierte CSS-Eigenschaften“ lesen.) Ich sollte erwähnen, dass ich mich (in einigen Projekten) für die Verwendung von SASS/SCSS-Variablen für diesen Zweck entschieden habe. Ich habe keinen wirklichen Vorteil gefunden, daher füge ich SASS normalerweise nur dann in ein Projekt ein, wenn ich finde, dass ich andere SASS-Funktionen benötige (Mix-Ins, Iteration, Importieren von Dateien usw.). Im Gegensatz dazu funktionieren benutzerdefinierte CSS-Eigenschaften auch mit der Kaskade und können im Laufe der Zeit geändert werden, anstatt sie statisch zu kompilieren. Bleiben wir also für heute bei einfachem CSS .
Erstellen wir in unserem Verzeichnis styles/
eine neue Datei 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; }
Natürlich kann und wird diese Liste mit der Zeit wachsen. Sobald wir diese Datei hinzugefügt haben, müssen wir zu unserer Datei pages/_app.jsx springen , die das Hauptlayout für alle unsere Seiten ist, und hinzufügen:
import '../styles/design_tokens.css'
Ich betrachte Design-Token gerne als den Klebstoff, der die Konsistenz im gesamten Projekt aufrechterhält. Wir werden diese Variablen auf globaler Ebene sowie innerhalb einzelner Komponenten referenzieren, um eine einheitliche Designsprache zu gewährleisten.
Globale Stile
Als nächstes fügen wir unserer Website eine Seite hinzu! Springen wir in die Datei pages/index.jsx (das ist unsere Homepage). Wir löschen alle Boilerplates und fügen etwas hinzu wie:
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> }
Leider wird es ziemlich schlicht aussehen, also setzen wir einige globale Stile für grundlegende Elemente , zB <h1>
-Tags. (Ich betrachte diese Stile gerne als „angemessene globale Standardeinstellungen“.) Wir können sie in bestimmten Fällen außer Kraft setzen, aber sie geben eine gute Vorstellung davon, was wir wollen, wenn wir es nicht tun.
Ich füge dies in die Datei styles/globals.css ein (die standardmäßig aus Next.js stammt):
*, *::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%; }
Natürlich ist diese Version ziemlich einfach, aber meine globals.css -Datei muss normalerweise nicht zu groß werden. Hier gestalte ich grundlegende HTML-Elemente (Überschriften, Textkörper, Links usw.). Es besteht keine Notwendigkeit, diese Elemente in React-Komponenten zu verpacken oder ständig Klassen hinzuzufügen, nur um einen grundlegenden Stil bereitzustellen.
Ich schließe auch alle Zurücksetzungen von Standard-Browser-Stilen ein. Gelegentlich werde ich einen seitenweiten Layoutstil haben, um beispielsweise eine „klebrige Fußzeile“ bereitzustellen, aber sie gehören nur hierher, wenn alle Seiten dasselbe Layout haben. Andernfalls muss der Bereich innerhalb einzelner Komponenten festgelegt werden.
Ich füge immer eine Art :focus
-Stil ein, um interaktive Elemente für Tastaturbenutzer deutlich anzuzeigen, wenn sie fokussiert sind. Es ist am besten, es zu einem festen Bestandteil der Design-DNA der Website zu machen!
Jetzt nimmt unsere Website Gestalt an:
Gebrauchsklassen
Ein Bereich, in dem unsere Homepage sicherlich verbessert werden könnte, ist, dass sich der Text derzeit immer bis zu den Seiten des Bildschirms erstreckt, also lassen Sie uns seine Breite begrenzen. Wir brauchen dieses Layout auf dieser Seite, aber ich kann mir vorstellen, dass wir es auch auf anderen Seiten brauchen werden. Dies ist ein großartiger Anwendungsfall für eine Utility-Klasse!
Ich versuche, Utility-Klassen sparsam zu verwenden und nicht nur als Ersatz für das Schreiben von CSS. Meine persönlichen Kriterien, wann es sinnvoll ist, ein Projekt zu ergänzen, sind:
- Ich brauche es immer wieder;
- Es macht eine Sache gut;
- Es gilt für eine Reihe verschiedener Komponenten oder Seiten.
Ich denke, dieser Fall erfüllt alle drei Kriterien, also erstellen wir eine neue CSS-Datei styles/utilities.css und fügen hinzu:
.lockup { max-width: 90ch; margin: 0 auto; }
Dann fügen wir import '../styles/utilities.css'
zu unseren Seiten/_app.jsx hinzu . Abschließend ändern wir das <main>
-Tag in unserer pages/index.jsx in <main className="lockup">
.
Jetzt wächst unsere Seite noch mehr zusammen. Da wir die Eigenschaft max-width
verwendet haben, benötigen wir keine Medienabfragen, um unser Layout für Mobilgeräte ansprechend zu machen. Und da wir die Maßeinheit ch
verwendet haben – was etwa der Breite eines Zeichens entspricht – passt sich unsere Größenanpassung dynamisch an die Schriftgröße des Browsers des Benutzers an.
Wenn unsere Website wächst, können wir weitere Utility-Klassen hinzufügen. Ich gehe hier ziemlich utilitaristisch vor: Wenn ich arbeite und feststelle, dass ich eine andere Klasse für eine Farbe oder etwas brauche, füge ich sie hinzu. Ich füge nicht jede mögliche Klasse unter der Sonne hinzu – das würde die CSS-Dateigröße aufblähen und meinen Code verwirrend machen. Manchmal, in größeren Projekten, teile ich die Dinge gerne in ein styles/utilities/
-Verzeichnis mit ein paar verschiedenen Dateien auf; es liegt an den Bedürfnissen des Projekts.
Wir können uns Utility-Klassen als unser Toolkit mit gemeinsamen, wiederholten Styling-Befehlen vorstellen , die global geteilt werden. Sie verhindern, dass wir ständig dasselbe CSS zwischen verschiedenen Komponenten neu schreiben.
Komponentenstile
Wir haben unsere Homepage für den Moment fertiggestellt, aber wir müssen noch einen Teil unserer Website aufbauen: den Online-Shop. Unser Ziel hier ist es, ein Kartenraster aller Tees anzuzeigen, die wir verkaufen möchten , also müssen wir unserer Website einige Komponenten hinzufügen.
Beginnen wir mit dem Hinzufügen einer neuen Seite unter pages/shop.jsx :
export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }
Dann brauchen wir ein paar Tees zum Präsentieren. Wir werden für jeden Tee einen Namen, eine Beschreibung und ein Bild (im Verzeichnis public/) angeben:
const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]
Hinweis : Dies ist kein Artikel über das Abrufen von Daten, also haben wir den einfachen Weg genommen und ein Array am Anfang der Datei definiert.
Als nächstes müssen wir eine Komponente definieren, um unsere Tees anzuzeigen. Beginnen wir damit, ein components/
-Verzeichnis zu erstellen (Next.js erstellt dies standardmäßig nicht). Dann fügen wir ein Verzeichnis components/TeaList
. Für jede Komponente, die mehr als eine Datei benötigt, lege ich normalerweise alle zugehörigen Dateien in einen Ordner. Dadurch wird verhindert, dass unser components/
Ordner nicht mehr navigierbar wird.
Fügen wir nun unsere Datei components/TeaList/TeaList.jsx hinzu :
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
Der Zweck dieser Komponente besteht darin, unsere Tees zu durchlaufen und für jeden ein Listenelement anzuzeigen. Lassen Sie uns nun unsere Komponente components/TeaList/TeaListItem.jsx definieren:
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
Beachten Sie, dass wir die integrierte Bildkomponente von Next.js verwenden. Ich setze das alt
-Attribut auf einen leeren String, weil die Bilder in diesem Fall rein dekorativ sind; Wir möchten vermeiden, dass Screenreader-Benutzer hier mit langen Bildbeschreibungen überfordert werden.
Lassen Sie uns abschließend eine Datei components/TeaList/index.js erstellen, damit unsere Komponenten einfach extern importiert werden können:
import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList
Und dann fügen wir alles zusammen, indem wir import TeaList from ../components/TeaList
und ein <TeaList teas={teas} />
-Element zu unserer Shop-Seite hinzufügen. Jetzt werden unsere Tees in einer Liste angezeigt, aber es wird nicht so schön sein.
Colocating-Stil mit Komponenten durch CSS-Module
Beginnen wir damit, unsere Karten (die TeaListLitem
Komponente) zu stylen. Jetzt möchten wir zum ersten Mal in unserem Projekt einen Stil hinzufügen, der nur für eine Komponente spezifisch ist. Erstellen wir eine neue Datei components/TeaList/TeaListItem.module.css .
Sie wundern sich vielleicht über das Modul in der Dateierweiterung. Dies ist ein CSS-Modul . Next.js unterstützt CSS-Module und enthält eine gute Dokumentation dazu. Wenn wir einen Klassennamen aus einem CSS-Modul wie .TeaListItem
, wird er automatisch in etwas umgewandelt, das eher . TeaListItem_TeaListItem__TFOk_
. TeaListItem_TeaListItem__TFOk_
mit einem Haufen zusätzlicher Zeichen angehängt. Folglich können wir jeden gewünschten Klassennamen verwenden, ohne befürchten zu müssen, dass er mit anderen Klassennamen an anderer Stelle auf unserer Site in Konflikt gerät.
Ein weiterer Vorteil von CSS-Modulen ist die Leistung. Next.js enthält eine dynamische Importfunktion. Mit next/dynamic können wir Komponenten faul laden, sodass ihr Code nur geladen wird, wenn er benötigt wird, anstatt ihn zur gesamten Bundle-Größe hinzuzufügen. Wenn wir die notwendigen lokalen Stile in einzelne Komponenten importieren, können Benutzer auch das CSS für dynamisch importierte Komponenten träge laden . Bei großen Projekten können wir uns dafür entscheiden, erhebliche Teile unseres Codes faul zu laden und nur das notwendigste JS/CSS im Voraus zu laden. Infolgedessen erstelle ich normalerweise eine neue CSS-Moduldatei für jede neue Komponente, die lokales Styling benötigt.
Beginnen wir damit, unserer Datei einige anfängliche Stile hinzuzufügen:
.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); }
Dann können wir den Stil aus ./TeaListItem.module.css
in unsere TeaListitem
Komponente importieren. Die style-Variable kommt wie ein JavaScript-Objekt herein, sodass wir auf dieses style.TeaListItem.
Hinweis : Unser Klassenname muss nicht groß geschrieben werden. Ich habe festgestellt, dass eine Konvention von großgeschriebenen Klassennamen innerhalb von Modulen (und Kleinbuchstaben außerhalb) zwischen lokalen und globalen Klassennamen visuell unterscheidet.
Nehmen wir also unsere neue lokale Klasse und weisen sie <li>
in unserer TeaListItem
Komponente zu:
<li className={style.TeaListComponent}>
Sie wundern sich vielleicht über die Linie der Hintergrundfarbe (dh var(--color, var(--off-white));
). Dieses Snippet bedeutet, dass der Hintergrund standardmäßig unser --off-white
Wert ist. Wenn wir jedoch eine benutzerdefinierte Eigenschaft --color
auf einer Karte festlegen, wird dieser Wert überschrieben und stattdessen ausgewählt.
Zuerst möchten wir, dass alle unsere Karten --off-white
sind, aber wir möchten später vielleicht den Wert für einzelne Karten ändern. Dies funktioniert sehr ähnlich wie Requisiten in React. Wir können einen Standardwert festlegen, aber einen Slot erstellen, in dem wir unter bestimmten Umständen andere Werte auswählen können. Ich ermutige uns also, an benutzerdefinierte CSS-Eigenschaften wie die CSS-Version von props zu denken .
Der Stil wird immer noch nicht gut aussehen, weil wir sicherstellen möchten, dass die Bilder in ihren Containern bleiben. Die Image-Komponente von Next.js mit der Eigenschaft layout="fill"
erhält position: absolute;
aus dem Framework, sodass wir die Größe begrenzen können, indem wir einen Container mit position: relative; einfügen.
Fügen wir unserer TeaListItem.module.css eine neue Klasse hinzu:
.ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }
Und dann fügen className={styles.ImageContainer}
zu dem <div>
hinzu, das unser <Image>
enthält. Ich verwende relativ „einfache“ Namen wie ImageContainer
, weil wir uns innerhalb eines CSS-Moduls befinden und wir uns keine Gedanken über Konflikte mit dem äußeren Stil machen müssen.
Schließlich möchten wir an den Seiten des Textes etwas Polsterung hinzufügen , also fügen wir eine letzte Klasse hinzu und verlassen uns auf die Abstandsvariablen, die wir als Design-Token eingerichtet haben:
.Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }
Wir können diese Klasse dem <div>
hinzufügen, das unseren Namen und unsere Beschreibung enthält. Nun, unsere Karten sehen gar nicht so schlecht aus:
Kombinieren von globalem und lokalem Stil
Als Nächstes möchten wir, dass unsere Karten in einem Rasterlayout angezeigt werden. In diesem Fall befinden wir uns gerade an der Grenze zwischen lokalen und globalen Stilen. Wir könnten unser Layout sicherlich direkt auf der TeaList
Komponente codieren. Aber ich könnte mir auch vorstellen, dass eine Hilfsklasse, die eine Liste in ein Rasterlayout umwandelt , an mehreren anderen Stellen nützlich sein könnte.
Nehmen wir hier den globalen Ansatz und fügen eine neue Utility-Klasse in unserer styles/utilities.css hinzu:
.grid { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--min-item-width, 30ch), 1fr)); gap: var(--space-md); }
Jetzt können wir die .grid
-Klasse zu jeder Liste hinzufügen und erhalten ein automatisch reagierendes Grid-Layout. Wir können auch die benutzerdefinierte Eigenschaft --min-item-width
(standardmäßig 30ch
) ändern, um die Mindestbreite jedes Elements zu ändern.
Hinweis : Denken Sie daran, an benutzerdefinierte Eigenschaften wie Requisiten zu denken! Wenn Ihnen diese Syntax nicht vertraut vorkommt, können Sie „Intrinsically Responsive CSS Grid With minmax()
And min()
“ von Chris Coyier ausprobieren.
Da wir diesen Stil global geschrieben haben, bedarf es keiner Extravaganz, um className="grid"
zu unserer TeaList
Komponente hinzuzufügen. Aber nehmen wir an, wir möchten diesen globalen Stil mit einem zusätzlichen lokalen Geschäft koppeln. Wir wollen zum Beispiel etwas mehr „Tee-Ästhetik“ hineinbringen und jede zweite Karte grün unterlegen. Alles, was wir tun müssten, ist eine neue Datei components/TeaList/TeaList.module.css zu erstellen:
.TeaList > :nth-child(even) { --color: var(--green); }
Erinnern Sie sich, wie wir eine --color custom
Eigenschaft --color für unsere TeaListItem
Komponente erstellt haben? Nun, jetzt können wir es unter bestimmten Umständen einstellen. Beachten Sie, dass wir immer noch untergeordnete Selektoren innerhalb von CSS-Modulen verwenden können, und es spielt keine Rolle, dass wir ein Element auswählen, das in einem anderen Modul formatiert ist. Wir können also auch unsere lokalen Komponentenstile verwenden, um untergeordnete Komponenten zu beeinflussen. Dies ist eher ein Feature als ein Fehler, da es uns ermöglicht, die CSS-Kaskade zu nutzen ! Wenn wir versuchen würden, diesen Effekt auf andere Weise zu replizieren, würden wir wahrscheinlich eher eine Art JavaScript-Suppe als drei Zeilen CSS erhalten.
Wie können wir dann die globale .grid
-Klasse in unserer TeaList
Komponente beibehalten und gleichzeitig die lokale .TeaList
-Klasse hinzufügen? An dieser Stelle kann die Syntax etwas unkonventionell werden, da wir über das CSS-Modul auf unsere .TeaList
-Klasse zugreifen müssen, indem Sie so etwas wie style.TeaList
.
Eine Option wäre die Verwendung der Zeichenfolgeninterpolation, um Folgendes zu erhalten:
<ul role="list" className={`${style.TeaList} grid`}>
In diesem kleinen Fall könnte dies gut genug sein. Wenn wir mehr Klassen mischen und anpassen, stelle ich fest, dass diese Syntax mein Gehirn ein wenig zum Explodieren bringt, also entscheide ich mich manchmal dafür, die Klassennamenbibliothek zu verwenden. In diesem Fall erhalten wir eine sinnvoller aussehende Liste:
<ul role="list" className={classnames(style.TeaList, "grid")}>
Jetzt haben wir unsere Shop-Seite fertiggestellt und unsere TeaList
Komponente so gestaltet, dass sie sowohl globale als auch lokale Stile nutzt.
Ein Balanceakt
Wir haben jetzt unseren Teeladen nur mit einfachem CSS erstellt, um das Styling zu handhaben. Sie haben vielleicht bemerkt, dass wir uns nicht ewig mit benutzerdefinierten Webpack-Setups, der Installation externer Bibliotheken und so weiter beschäftigen mussten. Das liegt an den von uns verwendeten Mustern, die sofort mit Next.js funktionieren. Darüber hinaus fördern sie bewährte CSS-Praktiken und fügen sich auf natürliche Weise in die Next.js-Framework-Architektur ein.
Unsere CSS-Organisation bestand aus vier Schlüsselelementen:
- Design-Token,
- Globale Stile,
- Gebrauchsklassen,
- Komponentenstile.
Während wir unsere Website weiter aufbauen, wird unsere Liste von Design-Tokens und Utility-Klassen wachsen. Jedes Styling, das nicht sinnvoll als Hilfsklasse hinzugefügt werden kann, können wir mithilfe von CSS-Modulen in Komponentenstile einfügen. Dadurch können wir ein kontinuierliches Gleichgewicht zwischen lokalen und globalen Styling-Belangen finden. Wir können auch leistungsstarken, intuitiven CSS-Code generieren , der neben unserer Next.js-Site auf natürliche Weise wächst.