Globalna a lokalna stylizacja w Next.js
Opublikowany: 2022-03-10Mam duże doświadczenie w używaniu Next.js do zarządzania złożonymi projektami front-endowymi. Next.js ma opinię na temat organizowania kodu JavaScript, ale nie ma wbudowanych opinii o tym, jak organizować CSS.
Po pracy w ramach tego frameworka znalazłem szereg wzorców organizacyjnych, które, jak sądzę, są zgodne zarówno z filozofią przewodnią Next.js, jak i stosują najlepsze praktyki CSS. W tym artykule wspólnie zbudujemy stronę internetową (sklep z herbatą!), aby zademonstrować te wzorce.
Uwaga : Prawdopodobnie nie będziesz potrzebować wcześniejszego doświadczenia z Next.js, chociaż dobrze byłoby mieć podstawową wiedzę na temat Reacta i być otwartym na naukę nowych technik CSS.
Pisanie „staromodnego” CSS
Przy pierwszym spojrzeniu na Next.js możemy pokusić się o rozważenie użycia jakiegoś rodzaju biblioteki CSS-in-JS. Chociaż mogą być korzyści w zależności od projektu, CSS-in-JS wprowadza wiele kwestii technicznych. Wymaga użycia nowej biblioteki zewnętrznej, co zwiększa rozmiar pakietu. CSS-in-JS może również mieć wpływ na wydajność, powodując dodatkowe renderowanie i zależności od stanu globalnego.
Zalecana lektura : „ Niewidoczne koszty wydajności nowoczesnych bibliotek CSS-in-JS w aplikacjach React)” autorstwa Aggelosa Arvanitakisa
Co więcej, celem korzystania z biblioteki takiej jak Next.js jest statyczne renderowanie zasobów, gdy tylko jest to możliwe, więc nie ma sensu pisanie JS, który musi być uruchamiany w przeglądarce, aby wygenerować CSS.
Jest kilka pytań, które musimy wziąć pod uwagę podczas organizowania stylu w Next.js:
Jak możemy dopasować się do konwencji/najlepszych praktyk ram?
Jak możemy zrównoważyć kwestie stylizacji „globalnej” (czcionki, kolory, główne układy itd.) z „lokalnymi” (stylami dotyczącymi poszczególnych komponentów)?
Odpowiedź, którą wymyśliłem na pierwsze pytanie, to po prostu napisać dobry, staromodny CSS . Next.js obsługuje to nie tylko bez dodatkowej konfiguracji; daje również wyniki, które są wydajne i statyczne.
Aby rozwiązać drugi problem, przyjmuję podejście, które można podsumować w czterech częściach:
- Żetony projektowe
- Globalne style
- Klasy użytkowe
- Style komponentów
Jestem wdzięczny pomysłowi Andy'ego Bella na temat CUBE CSS („Kompozycja, użyteczność, blokowanie, wyjątek”) tutaj. Jeśli nie słyszałeś wcześniej o tej zasadzie organizacyjnej, polecam sprawdzić jej oficjalną stronę lub funkcję w Smashing Podcast. Jedną z zasad, które zaczerpniemy z CUBE CSS, jest idea, że powinniśmy przyjąć kaskadę CSS, a nie bać się jej. Nauczmy się tych technik, stosując je do projektu strony internetowej.
Pierwsze kroki
Będziemy budować sklep z herbatą, bo cóż, herbata jest smaczna. Zaczniemy od uruchomienia yarn create next-app
, aby stworzyć nowy projekt Next.js. Następnie usuniemy wszystko z styles/ directory
(to cały przykładowy kod).
Uwaga : Jeśli chcesz śledzić wraz z gotowym projektem, możesz to sprawdzić tutaj.
Żetony projektowe
W prawie każdej konfiguracji CSS istnieje wyraźna korzyść z przechowywania wszystkich globalnie udostępnianych wartości w zmiennych . Jeśli klient prosi o zmianę koloru, wdrożenie zmiany jest raczej jednolinijką niż ogromnym bałaganem typu „znajdź i zamień”. W związku z tym kluczową częścią naszej konfiguracji CSS Next.js będzie przechowywanie wszystkich wartości z całej witryny jako tokenów projektu .
Do przechowywania tych tokenów użyjemy wbudowanych właściwości niestandardowych CSS. (Jeśli nie znasz tej składni, możesz zajrzeć do „Przewodnika strategii po właściwościach niestandardowych CSS”.) Powinienem wspomnieć, że (w niektórych projektach) zdecydowałem się użyć w tym celu zmiennych SASS/SCSS. Nie znalazłem żadnej rzeczywistej przewagi, więc zwykle włączam SASS do projektu tylko wtedy, gdy potrzebuję innych funkcji SASS (domieszki, iteracje, importowanie plików itp.). W przeciwieństwie do tego, niestandardowe właściwości CSS również działają z kaskadą i mogą być zmieniane w czasie, a nie statycznie kompilowane. Więc na dziś zostańmy przy czystym CSS .
W naszym katalogu styles/
stwórzmy nowy plik 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; }
Oczywiście ta lista może i będzie z czasem rosnąć. Po dodaniu tego pliku musimy przeskoczyć do naszego pliku pages/_app.jsx , który jest głównym układem wszystkich naszych stron, i dodać:
import '../styles/design_tokens.css'
Lubię myśleć o tokenach projektowych jako o kleju, który utrzymuje spójność w całym projekcie. Będziemy odnosić się do tych zmiennych w skali globalnej, a także w ramach poszczególnych komponentów, zapewniając ujednolicony język projektowania.
Style globalne
Następnie dodajmy stronę do naszej witryny! Wskoczmy do pliku pages/index.jsx (to jest nasza strona domowa). Usuniemy cały boilerplate i dodamy coś takiego:
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> }
Niestety będzie to wyglądało dość prosto, więc ustawmy kilka globalnych stylów dla podstawowych elementów , np. tagów <h1>
. (Lubię myśleć o tych stylach jako o „rozsądnych globalnych wartościach domyślnych”). Możemy je zastąpić w określonych przypadkach, ale są one dobrym przypuszczeniem, czego będziemy chcieli, jeśli tego nie zrobimy.
Umieszczę to w pliku styles/globals.css (który domyślnie pochodzi z 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%; }
Oczywiście ta wersja jest dość prosta, ale mój plik globals.css zwykle nie musi być zbyt duży. Tutaj stylizuję podstawowe elementy HTML (nagłówki, treść, linki itd.). Nie ma potrzeby owijania tych elementów w komponenty Reacta lub ciągłego dodawania klas tylko po to, aby zapewnić podstawowy styl.
Uwzględniam również wszelkie resety domyślnych stylów przeglądarki . Od czasu do czasu będę używał stylu układu obejmującego całą witrynę, aby zapewnić na przykład „przyklejoną stopkę”, ale należą one tutaj tylko wtedy, gdy wszystkie strony mają ten sam układ. W przeciwnym razie będzie musiał być objęty zakresem poszczególnych komponentów.
Zawsze dołączam jakiś styl :focus
, aby wyraźnie wskazać elementy interaktywne dla użytkowników klawiatury, gdy są skupieni. Najlepiej, aby była integralną częścią DNA designu witryny!
Teraz nasza strona internetowa zaczyna się kształtować:
Klasy użytkowe
Jednym z obszarów, w którym nasza strona główna z pewnością mogłaby się poprawić, jest to, że obecnie tekst zawsze rozciąga się na boki ekranu, więc ograniczmy jego szerokość. Potrzebujemy tego układu na tej stronie, ale wyobrażam sobie, że możemy go potrzebować również na innych stronach. To świetny przypadek użycia dla klasy użytkowej!
Staram się używać klas użytkowych oszczędnie , a nie jako zamiennika samego pisania CSS. Moje osobiste kryteria określające, kiedy dodanie go do projektu ma sens, to:
- Potrzebuję tego wielokrotnie;
- Jedna rzecz robi dobrze;
- Dotyczy wielu różnych komponentów lub stron.
Myślę, że ten przypadek spełnia wszystkie trzy kryteria, więc utwórzmy nowy plik CSS styles/utilities.css i dodajmy:
.lockup { max-width: 90ch; margin: 0 auto; }
Następnie dodajmy import '../styles/utilities.css'
do naszych stron/_app.jsx . Na koniec zmieńmy tag <main>
w naszym pages/index.jsx na <main className="lockup">
.
Teraz nasza strona łączy się jeszcze bardziej. Ponieważ użyliśmy właściwości max-width
, nie potrzebujemy żadnych zapytań o media, aby nasz układ był responsywny na urządzenia mobilne. A ponieważ użyliśmy jednostki miary ch
— która odpowiada mniej więcej szerokości jednego znaku — nasz rozmiar jest dynamiczny w stosunku do rozmiaru czcionki przeglądarki użytkownika.
Wraz z rozwojem naszej strony internetowej możemy dodawać kolejne klasy użytkowe. Przyjmuję tutaj dość utylitarne podejście: jeśli pracuję i stwierdzam, że potrzebuję innej klasy dla koloru lub czegoś, dodaję to. Nie dodaję każdej możliwej klasy pod słońcem — zwiększyłoby to rozmiar pliku CSS i sprawiłoby, że mój kod byłby zagmatwany. Czasami, w większych projektach, lubię dzielić rzeczy na katalog styles/utilities/
z kilkoma różnymi plikami; to zależy od potrzeb projektu.
Możemy myśleć o klasach użytkowych jako o naszym zestawie wspólnych, powtarzających się poleceń stylizacji, które są udostępniane globalnie. Pomagają nam zapobiegać ciągłemu przepisywania tego samego CSS między różnymi komponentami.
Style komponentów
Na razie zakończyliśmy naszą stronę główną, ale nadal musimy zbudować fragment naszej strony internetowej: sklep internetowy. Naszym celem będzie tutaj wyświetlenie siatki kart wszystkich herbat, które chcemy sprzedawać , więc będziemy musieli dodać kilka komponentów do naszej strony.
Zacznijmy od dodania nowej strony na pages/shop.jsx :
export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }
Następnie będziemy potrzebować herbat do wyświetlenia. Do każdej herbaty dołączymy nazwę, opis i obraz (w katalogu public/):
const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]
Uwaga : To nie jest artykuł o pobieraniu danych, więc wybraliśmy łatwą drogę i zdefiniowaliśmy tablicę na początku pliku.
Następnie musimy zdefiniować komponent do wyświetlania naszych herbat. Zacznijmy od utworzenia katalogu components/
(Next.js nie robi tego domyślnie). Następnie dodajmy katalog components/TeaList
. W przypadku każdego komponentu, który ostatecznie potrzebuje więcej niż jednego pliku, zwykle umieszczam wszystkie powiązane pliki w folderze. W ten sposób uniemożliwisz nawigację naszych components/
folderów.
Teraz dodajmy nasze komponenty/TeaList/TeaList.jsx plik:
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
Celem tego komponentu jest iteracja po naszych herbatach i pokazanie pozycji listy dla każdej z nich, więc teraz zdefiniujmy nasz komponent components/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
Zauważ, że używamy wbudowanego komponentu obrazu Next.js. Ustawiłem atrybut alt
na pusty ciąg, ponieważ w tym przypadku obrazy są czysto dekoracyjne; chcemy uniknąć zapychania użytkowników czytników ekranu długimi opisami obrazów.
Na koniec stwórzmy plik components/TeaList/index.js , aby nasze komponenty były łatwe do importowania z zewnątrz:
import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList
A potem połączmy to wszystko razem, dodając import TeaList z ../components/TeaList
i element <TeaList teas={teas} />
do naszej strony Sklepu. Teraz nasze herbaty pojawią się na liście, ale nie będzie tak ładnie.
Kolokacja stylu z komponentami za pomocą modułów CSS
Zacznijmy od nadawania stylu naszym kartom (komponent TeaListLitem
). Teraz, po raz pierwszy w naszym projekcie, będziemy chcieli dodać styl charakterystyczny tylko dla jednego komponentu. Stwórzmy nowy plik components/TeaList/TeaListItem.module.css .
Być może zastanawiasz się nad modułem w rozszerzeniu pliku. To jest moduł CSS . Next.js obsługuje moduły CSS i zawiera dobrą dokumentację na ich temat. Kiedy napiszemy nazwę klasy z modułu CSS, takiego jak .TeaListItem
, zostanie ona automatycznie przekształcona w coś bardziej podobnego . TeaListItem_TeaListItem__TFOk_
. TeaListItem_TeaListItem__TFOk_
z doczepionymi dodatkowymi znakami. W związku z tym możemy użyć dowolnej nazwy klasy bez obawy, że będzie ona kolidować z innymi nazwami klas w innych miejscach naszej witryny.
Kolejną zaletą modułów CSS jest wydajność. Next.js zawiera funkcję dynamicznego importu. next/dynamic pozwala nam na leniwe ładowanie komponentów, dzięki czemu ich kod jest ładowany tylko wtedy, gdy jest to potrzebne, zamiast dodawać do całego rozmiaru pakietu. Jeśli zaimportujemy niezbędne style lokalne do poszczególnych komponentów, użytkownicy mogą również leniwie załadować CSS dla komponentów importowanych dynamicznie . W przypadku dużych projektów możemy zdecydować się na leniwe ładowanie znacznych fragmentów naszego kodu i ładowanie tylko najbardziej niezbędnych z góry JS/CSS. W rezultacie zazwyczaj tworzę nowy plik modułu CSS dla każdego nowego komponentu, który wymaga lokalnego stylizacji.
Zacznijmy od dodania kilku początkowych stylów do naszego pliku:
.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); }
Następnie możemy zaimportować styl z ./TeaListItem.module.css
w naszym komponencie TeaListitem
. Zmienna style działa jak obiekt JavaScript, więc możemy uzyskać dostęp do tego klasowego style.TeaListItem.
Uwaga : nazwa naszej klasy nie musi być pisana wielkimi literami. Odkryłem, że konwencja nazw klas pisanych wielką literą wewnątrz modułów (i na zewnątrz małych liter) rozróżnia wizualnie nazwy klas lokalnych i globalnych.
Weźmy więc naszą nową klasę lokalną i przypiszmy ją do <li>
w naszym komponencie TeaListItem
:
<li className={style.TeaListComponent}>
Być może zastanawiasz się nad linią koloru tła (np var(--color, var(--off-white));
). Ten fragment kodu oznacza, że domyślnie tło będzie naszą wartością --off-white
. Ale jeśli ustawimy niestandardową właściwość --color
na karcie, zastąpi ona i zamiast tego wybierze tę wartość.
Na początku chcemy, aby wszystkie nasze karty były --off-white
, ale możemy później zmienić wartość dla poszczególnych kart. Działa to bardzo podobnie do rekwizytów w React. Możemy ustawić wartość domyślną, ale stworzyć slot, w którym możemy wybrać inne wartości w określonych okolicznościach. Dlatego zachęcam nas do myślenia o niestandardowych właściwościach CSS, takich jak wersja props w CSS .
Styl nadal nie będzie wyglądał dobrze, ponieważ chcemy mieć pewność, że obrazy pozostaną w swoich pojemnikach. Komponent Image Next.js z właściwością layout="fill"
otrzymuje position: absolute;
z frameworka, więc możemy ograniczyć rozmiar umieszczając w kontenerze z pozycją: relative;.
Dodajmy nową klasę do naszego TeaListItem.module.css :
.ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }
A następnie dodajmy className={styles.ImageContainer}
do elementu <div>
zawierającego nasz <Image>
. Używam stosunkowo „prostych” nazw, takich jak ImageContainer
, ponieważ jesteśmy wewnątrz modułu CSS, więc nie musimy się martwić o konflikt ze stylem zewnętrznym.
Na koniec chcemy dodać trochę wypełnienia po bokach tekstu, więc dodajmy ostatnią klasę i polegajmy na zmiennych odstępów, które ustawiliśmy jako tokeny projektu:
.Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }
Możemy dodać tę klasę do <div>
, który zawiera naszą nazwę i opis. Teraz nasze karty nie wyglądają tak źle:
Łączenie stylu globalnego i lokalnego
Następnie chcemy, aby nasze karty były wyświetlane w układzie siatki. W tym przypadku jesteśmy na pograniczu stylu lokalnego i globalnego. Z pewnością moglibyśmy zakodować nasz układ bezpośrednio w komponencie TeaList
. Ale mógłbym również wyobrazić sobie, że posiadanie klasy użytkowej, która zamienia listę w układ siatki, może być przydatne w kilku innych miejscach.
Przyjmijmy podejście globalne i dodajmy nową klasę narzędziową w naszym 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); }
Teraz możemy dodać klasę .grid
do dowolnej listy, a otrzymamy automatycznie reagujący układ siatki. Możemy również zmienić niestandardową właściwość --min-item-width
(domyślnie 30ch
), aby zmienić minimalną szerokość każdego elementu.
Uwaga : Pamiętaj, aby myśleć o właściwościach niestandardowych, takich jak rekwizyty! Jeśli ta składnia wygląda na nieznaną, możesz sprawdzić „Intrinsically Responsive CSS Grid With minmax()
And min()
” autorstwa Chrisa Coyiera.
Ponieważ pisaliśmy ten styl globalnie, dodanie className="grid"
do naszego komponentu TeaList
nie wymaga żadnych wymyśleń. Ale powiedzmy, że chcemy połączyć ten globalny styl z dodatkowym lokalnym sklepem. Na przykład chcemy wprowadzić nieco więcej „estetyki herbaty” i sprawić, by każda inna karta miała zielone tło. Wystarczy, że stworzymy nowy plik component/TeaList/TeaList.module.css :
.TeaList > :nth-child(even) { --color: var(--green); }
Pamiętasz, jak --color custom
właściwość --color w naszym komponencie TeaListItem
? Cóż, teraz możemy to ustawić w określonych okolicznościach. Zauważ, że nadal możemy używać selektorów podrzędnych w modułach CSS i nie ma znaczenia, że wybieramy element, który jest stylizowany wewnątrz innego modułu. Tak więc możemy również użyć naszych lokalnych stylów komponentów, aby wpłynąć na komponenty podrzędne. Jest to cecha, a nie błąd, ponieważ pozwala nam wykorzystać kaskadę CSS ! Gdybyśmy spróbowali odtworzyć ten efekt w inny sposób, prawdopodobnie otrzymalibyśmy coś w rodzaju zupy JavaScript, a nie trzy wiersze CSS.
W takim razie, jak możemy zachować globalną klasę .grid
w naszym komponencie TeaList
, jednocześnie dodając lokalną klasę .TeaList
? W tym miejscu składnia może stać się nieco dziwna, ponieważ musimy uzyskać dostęp do naszej klasy .TeaList
z modułu CSS, wykonując coś takiego jak style.TeaList
.
Jedną z opcji byłoby użycie interpolacji ciągów, aby uzyskać coś takiego:
<ul role="list" className={`${style.TeaList} grid`}>
W tym małym przypadku może to wystarczyć. Jeśli mieszamy i dopasowujemy więcej klas, stwierdzam, że ta składnia powoduje, że mój mózg trochę eksploduje, więc czasami zdecyduję się na użycie biblioteki nazw klas. W tym przypadku otrzymujemy bardziej sensownie wyglądającą listę:
<ul role="list" className={classnames(style.TeaList, "grid")}>
Teraz zakończyliśmy naszą stronę Sklepu i sprawiliśmy, że nasz składnik TeaList
wykorzystuje zarówno globalne, jak i lokalne style.
Ustawa o równowadze
Teraz zbudowaliśmy naszą herbaciarnię, używając tylko zwykłego CSS do obsługi stylizacji. Być może zauważyłeś, że nie musieliśmy spędzać wieków na zajmowaniu się niestandardowymi konfiguracjami Webpacków, instalowaniem zewnętrznych bibliotek i tak dalej. Dzieje się tak ze względu na wzorce, których używaliśmy do pracy z Next.js po wyjęciu z pudełka. Ponadto zachęcają do najlepszych praktyk CSS i naturalnie pasują do architektury frameworka Next.js.
Nasza organizacja CSS składała się z czterech kluczowych elementów:
- Zaprojektuj tokeny,
- Globalne style,
- Klasy użytkowe,
- Style komponentów.
W miarę dalszego budowania naszej witryny, nasza lista tokenów projektowych i klas użyteczności będzie się powiększać. Wszelkie stylizacje, które nie mają sensu dodawać jako klasy użytkowej, możemy dodać do stylów komponentów za pomocą modułów CSS. W rezultacie możemy znaleźć ciągłą równowagę między lokalnymi i globalnymi problemami stylistycznymi. Możemy również generować wydajny, intuicyjny kod CSS, który rośnie naturalnie wraz z naszą witryną Next.js.