Глобальный и локальный стиль в Next.js
Опубликовано: 2022-03-10У меня большой опыт использования Next.js для управления сложными интерфейсными проектами. Next.js высказывает свое мнение о том, как организовать код JavaScript, но не имеет встроенных мнений о том, как организовать CSS.
Поработав в этой структуре, я обнаружил ряд организационных шаблонов, которые, по моему мнению, соответствуют основной философии Next.js и используют лучшие практики CSS. В этой статье мы вместе создадим веб-сайт (чайный магазин!), чтобы продемонстрировать эти шаблоны.
Примечание . Вам, вероятно, не потребуется предварительный опыт работы с Next.js, хотя было бы неплохо иметь базовое понимание React и быть готовым к изучению некоторых новых методов CSS.
Написание «старомодного» CSS
При первом знакомстве с Next.js у нас может возникнуть соблазн рассмотреть возможность использования какой-либо библиотеки CSS-in-JS. Хотя в зависимости от проекта могут быть преимущества, CSS-in-JS вводит множество технических соображений. Это требует использования новой внешней библиотеки, которая увеличивает размер пакета. CSS-in-JS также может влиять на производительность, вызывая дополнительные отрисовки и зависимости от глобального состояния.
Рекомендуемое чтение : « Невидимые затраты на производительность современных библиотек CSS-in-JS в приложениях React)», автор Aggelos Arvanitakis.
Кроме того, весь смысл использования такой библиотеки, как Next.js, заключается в статическом отображении ресурсов, когда это возможно, поэтому не имеет особого смысла писать JS, который нужно запускать в браузере для генерации CSS.
Есть несколько вопросов, которые мы должны рассмотреть при организации стиля в Next.js:
Как мы можем вписаться в соглашения/лучшие практики структуры?
Как мы можем сбалансировать «глобальные» аспекты стиля (шрифты, цвета, основные макеты и т. д.) с «локальными» (стили, относящиеся к отдельным компонентам)?
Ответ, который я придумал на первый вопрос, заключается в том, чтобы просто написать старый добрый CSS . Next.js не только поддерживает это без дополнительной настройки; это также дает результаты, которые являются производительными и статическими.
Для решения второй проблемы я использую подход, который можно свести к четырем частям:
- Маркеры дизайна
- Глобальные стили
- Вспомогательные классы
- Стили компонентов
Здесь я в долгу перед идеей Энди Белла о CUBE CSS («Композиция, Утилита, Блок, Исключение»). Если вы раньше не слышали об этом организационном принципе, я рекомендую заглянуть на его официальный сайт или в рубрику Smashing Podcast. Один из принципов, который мы возьмем из CUBE CSS, заключается в том, что мы должны принять каскад CSS, а не бояться его. Давайте изучим эти методы, применив их к проекту веб-сайта.
Начиная
Мы построим чайный магазин, потому что чай вкусный. Мы начнем с запуска yarn create next-app
для создания нового проекта Next.js. Затем мы удалим все в styles/ directory
(это всего лишь пример кода).
Примечание . Если вы хотите следить за готовым проектом, вы можете проверить его здесь.
Жетоны дизайна
Практически в любой настройке CSS есть явное преимущество хранения всех глобально общих значений в переменных . Если клиент просит изменить цвет, реализация изменения — это однострочная работа, а не массовый беспорядок с поиском и заменой. Следовательно, ключевой частью нашей настройки CSS Next.js будет сохранение всех значений для всего сайта в виде токенов дизайна .
Мы будем использовать встроенные пользовательские свойства CSS для хранения этих токенов. (Если вы не знакомы с этим синтаксисом, вы можете ознакомиться с «Руководством по стратегии для пользовательских свойств CSS».) Я должен упомянуть, что (в некоторых проектах) я решил использовать для этой цели переменные SASS/SCSS. Я не нашел никаких реальных преимуществ, поэтому обычно включаю SASS в проект только в том случае, если мне нужны другие функции SASS (примеси, итерация, импорт файлов и т. д.). Пользовательские свойства CSS, напротив, также работают с каскадом и могут изменяться с течением времени, а не статически компилироваться. Итак, на сегодня давайте придерживаться простого CSS .
В нашем каталоге styles/
давайте создадим новый файл 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; }
Конечно, этот список может и будет расти со временем. Как только мы добавим этот файл, нам нужно перейти к нашему файлу pages/_app.jsx , который является основным макетом для всех наших страниц, и добавить:
import '../styles/design_tokens.css'
Мне нравится думать о токенах дизайна как о клее, который поддерживает согласованность в проекте. Мы будем ссылаться на эти переменные в глобальном масштабе, а также в отдельных компонентах, обеспечивая единый язык дизайна.
Глобальные стили
Далее, давайте добавим страницу на наш сайт! Давайте перейдем к файлу pages/index.jsx (это наша домашняя страница). Мы удалим весь шаблон и добавим что-то вроде:
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> }
К сожалению, это будет выглядеть довольно просто, поэтому давайте зададим некоторые глобальные стили для базовых элементов , например теги <h1>
. (Мне нравится думать об этих стилях как о «разумных глобальных значениях по умолчанию».) Мы можем переопределить их в определенных случаях, но они дают хорошее представление о том, что нам нужно, если мы этого не сделаем.
Я помещу это в файл styles/globals.css (который по умолчанию поставляется из 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%; }
Конечно, эта версия довольно проста, но мой файл globals.css обычно не становится слишком большим. Здесь я стилизую базовые HTML-элементы (заголовки, текст, ссылки и т. д.). Нет необходимости оборачивать эти элементы в компоненты React или постоянно добавлять классы только для того, чтобы обеспечить базовый стиль.
Я также включаю любые сбросы стилей браузера по умолчанию . Иногда у меня будет какой-то стиль макета для всего сайта, например, чтобы обеспечить «липкий нижний колонтитул», но они подходят здесь только в том случае, если все страницы имеют один и тот же макет. В противном случае его необходимо будет ограничить отдельными компонентами.
Я всегда включаю какой-то стиль :focus
, чтобы четко указать на интерактивные элементы для пользователей клавиатуры, когда они сфокусированы. Лучше всего сделать его неотъемлемой частью ДНК дизайна сайта!
Теперь наш сайт начинает формироваться:
Вспомогательные классы
Одной из областей, где наша домашняя страница, безусловно, может быть улучшена, является то, что текст в настоящее время всегда распространяется по краям экрана, поэтому давайте ограничим его ширину. Нам нужен этот макет на этой странице, но я думаю, что он может понадобиться и на других страницах. Это отличный пример использования служебного класса!
Я стараюсь экономно использовать служебные классы , а не вместо простого написания CSS. Мои личные критерии того, когда имеет смысл добавить его в проект, таковы:
- Мне это нужно неоднократно;
- Он делает одну вещь хорошо;
- Он применяется к целому ряду различных компонентов или страниц.
Я думаю, что этот случай соответствует всем трем критериям, поэтому давайте создадим новый файл CSS styles/utilities.css и добавим:
.lockup { max-width: 90ch; margin: 0 auto; }
Затем добавим импорт '../styles/utilities.css'
на наши страницы/_app.jsx . Наконец, давайте изменим <main>
в наших страницах/index.jsx на <main className="lockup">
.
Теперь наша страница стала еще больше. Поскольку мы использовали свойство max-width
, нам не нужны никакие медиа-запросы, чтобы наш макет реагировал на мобильные устройства. И, поскольку мы использовали единицу измерения ch
, которая примерно соответствует ширине одного символа, наши размеры динамически зависят от размера шрифта браузера пользователя.
По мере роста нашего веб-сайта мы можем продолжать добавлять дополнительные служебные классы. Здесь я придерживаюсь довольно утилитарного подхода: если я работаю и обнаруживаю, что мне нужен еще один класс для цвета или чего-то еще, я добавляю его. Я не добавляю все возможные классы на свете — это увеличило бы размер файла CSS и сделало бы мой код запутанным. Иногда в больших проектах мне нравится разбивать вещи на каталог styles/utilities/
с несколькими разными файлами; это зависит от потребностей проекта.
Мы можем думать о служебных классах как о наборе общих, повторяющихся команд стилей , которые являются общими для всего мира. Они помогают нам не переписывать один и тот же CSS между разными компонентами.
Стили компонентов
На данный момент мы закончили нашу домашнюю страницу, но нам еще нужно создать часть нашего веб-сайта: интернет-магазин. Нашей целью здесь будет отобразить сетку карт всех чаев, которые мы хотим продать , поэтому нам нужно добавить некоторые компоненты на наш сайт.
Давайте начнем с добавления новой страницы в pages/shop.jsx :
export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }
Затем нам понадобится несколько чаев для демонстрации. Мы включим имя, описание и изображение (в каталоге public/) для каждого чая:
const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]
Примечание . Это не статья о выборке данных, поэтому мы пошли по простому пути и определили массив в начале файла.
Далее нам нужно определить компонент для отображения наших чаев. Начнем с создания каталога components/
(Next.js не создает его по умолчанию). Затем добавим каталог components/TeaList
. Для любого компонента, которому требуется более одного файла, я обычно помещаю все связанные файлы в папку. Это предотвратит невозможность навигации по нашим components/
папке.
Теперь давайте добавим наш файл component/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
Цель этого компонента состоит в том, чтобы перебирать наши чаи и отображать элемент списка для каждого из них, поэтому теперь давайте определим наш компонент component/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
Обратите внимание, что мы используем встроенный компонент изображения Next.js. Я установил для атрибута alt
пустую строку, поскольку в данном случае изображения носят исключительно декоративный характер; мы хотим не утомлять пользователей программ чтения с экрана длинными описаниями изображений.
Наконец, давайте создадим файл components/TeaList/index.js , чтобы наши компоненты можно было легко импортировать извне:
import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList
А затем давайте соединим все это вместе, добавив import TeaList из ../components/TeaList
и элемент <TeaList teas={teas} />
на нашу страницу магазина. Теперь наши чаи будут отображаться в списке, но это будет не так красиво.
Совместное размещение стиля с компонентами через модули CSS
Давайте начнем со стиля наших карточек (компонент TeaListLitem
). Теперь, впервые в нашем проекте, мы собираемся добавить стиль, относящийся только к одному компоненту. Давайте создадим новый файл components/TeaList/TeaListItem.module.css .
Вам может быть интересно узнать о модуле в расширении файла. Это CSS-модуль . Next.js поддерживает модули CSS и включает в себя хорошую документацию по ним. Когда мы записываем имя класса из модуля CSS, такого как .TeaListItem
, оно автоматически преобразуется во что-то более похожее на . TeaListItem_TeaListItem__TFOk_
. TeaListItem_TeaListItem__TFOk_
с кучей дополнительных символов. Следовательно, мы можем использовать любое имя класса, которое захотим , не беспокоясь о том, что оно будет конфликтовать с другими именами классов в других местах нашего сайта.
Еще одним преимуществом модулей CSS является производительность. Next.js включает функцию динамического импорта. next/dynamic позволяет нам лениво загружать компоненты, чтобы их код загружался только при необходимости, а не увеличивался до размера всего пакета. Если мы импортируем необходимые локальные стили в отдельные компоненты, то пользователи также могут лениво загружать CSS для динамически импортируемых компонентов . Для больших проектов мы можем выбрать ленивую загрузку значительных фрагментов нашего кода и загружать только самые необходимые JS/CSS заранее. В результате я обычно заканчиваю тем, что создаю новый файл модуля CSS для каждого нового компонента, который нуждается в локальном стиле.
Давайте начнем с добавления некоторых исходных стилей в наш файл:
.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); }
Затем мы можем импортировать стиль из ./TeaListItem.module.css
в наш компонент TeaListitem
. Переменная стиля входит как объект JavaScript, поэтому мы можем получить доступ к этому классу style.TeaListItem.
Примечание . Имя нашего класса не нужно писать с заглавной буквы. Я обнаружил, что соглашение об именах классов с заглавными буквами внутри модулей (и строчными буквами снаружи) визуально отличает локальные и глобальные имена классов.
Итак, давайте возьмем наш новый локальный класс и назначим его <li>
в нашем компоненте TeaListItem
:
<li className={style.TeaListComponent}>
Вас может интересовать линия цвета фона (например var(--color, var(--off-white));
). Этот фрагмент означает, что по умолчанию фоном будет наше --off-white
. Но если мы установим пользовательское свойство --color
на карточке, оно переопределит и вместо этого выберет это значение.
Сначала мы хотим, чтобы все наши карты были --off-white
, но позже мы можем изменить значение для отдельных карт. Это работает очень похоже на props в React. Мы можем установить значение по умолчанию, но создать слот, где мы можем выбрать другие значения в определенных обстоятельствах. Итак, я призываю нас думать о настраиваемых свойствах CSS, таких как CSS-версия props .
Стиль по-прежнему не будет выглядеть великолепно, потому что мы хотим убедиться, что изображения остаются в своих контейнерах. Компонент изображения Next.js с реквизитом layout="fill"
получает position: absolute;
из фреймворка, поэтому мы можем ограничить размер, поместив контейнер с position: relative;.
Давайте добавим новый класс в наш TeaListItem.module.css :
.ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }
А затем добавим className={styles.ImageContainer}
в <div>
, который содержит наш <Image>
. Я использую относительно «простые» имена, такие как ImageContainer
, потому что мы находимся внутри модуля CSS, поэтому нам не нужно беспокоиться о конфликте с внешним стилем.
Наконец, мы хотим добавить немного отступов по бокам текста, поэтому давайте добавим последний класс и будем полагаться на переменные интервала, которые мы настроили как маркеры дизайна:
.Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }
Мы можем добавить этот класс в <div>
, который содержит наше имя и описание. Теперь наши карты выглядят не так уж плохо:
Сочетание глобального и местного стиля
Затем мы хотим, чтобы наши карты отображались в виде сетки. В данном случае мы находимся как раз на границе между локальным и глобальным стилями. Конечно, мы могли бы закодировать наш макет непосредственно в компоненте TeaList
. Но я также мог предположить, что служебный класс, который превращает список в сетку, может быть полезен в нескольких других местах.
Давайте воспользуемся глобальным подходом и добавим новый служебный класс в наш 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); }
Теперь мы можем добавить класс .grid
в любой список, и мы получим автоматически реагирующий макет сетки. Мы также можем изменить пользовательское свойство --min-item-width
(по умолчанию 30ch
), чтобы изменить минимальную ширину каждого элемента.
Примечание . Не забывайте думать о пользовательских свойствах, таких как реквизит! Если этот синтаксис выглядит незнакомым, вы можете ознакомиться с «Внутренне адаптивной CSS-сеткой с minmax()
и min()
» Криса Койера.
Поскольку мы написали этот стиль глобально, не требуется никаких изысков, чтобы добавить className="grid"
в наш компонент TeaList
. Но, допустим, мы хотим соединить этот глобальный стиль с каким-то дополнительным локальным хранилищем. Например, мы хотим добавить немного «эстетики чая» и сделать так, чтобы все остальные карты имели зеленый фон. Все, что нам нужно сделать, это создать новый файл component/TeaList/TeaList.module.css :
.TeaList > :nth-child(even) { --color: var(--green); }
Помните, как мы создали --color custom
для нашего компонента TeaListItem
? Что ж, теперь мы можем установить его при определенных обстоятельствах. Обратите внимание, что мы по-прежнему можем использовать дочерние селекторы в модулях CSS, и не имеет значения, что мы выбираем элемент, стилизованный внутри другого модуля. Таким образом, мы также можем использовать наши локальные стили компонентов для воздействия на дочерние компоненты. Это функция, а не ошибка, поскольку она позволяет нам использовать каскад CSS ! Если бы мы попытались воспроизвести этот эффект каким-то другим способом, мы, скорее всего, получили бы какой-то суп из JavaScript, а не три строки CSS.
Тогда как мы можем сохранить глобальный класс .grid
в нашем компоненте TeaList
, а также добавить локальный класс .TeaList
? Здесь синтаксис может стать немного странным, потому что нам нужно получить доступ к нашему классу .TeaList
из модуля CSS, выполнив что-то вроде style.TeaList
.
Одним из вариантов было бы использовать интерполяцию строк, чтобы получить что-то вроде:
<ul role="list" className={`${style.TeaList} grid`}>
В этом маленьком случае этого может быть достаточно. Если мы смешиваем и сопоставляем больше классов, я обнаружил, что этот синтаксис заставляет мой мозг немного взорваться, поэтому я иногда предпочитаю использовать библиотеку имен классов. В этом случае мы получаем более разумный список:
<ul role="list" className={classnames(style.TeaList, "grid")}>
Теперь мы закончили работу со страницей магазина и сделали так, чтобы наш компонент TeaList
использовать как глобальные, так и локальные стили.
Балансирующий акт
Теперь мы создали нашу чайную, используя только простой CSS для обработки стилей. Вы могли заметить, что нам не пришлось тратить целую вечность на настройку пользовательских Webpack, установку внешних библиотек и так далее. Это связано с тем, что шаблоны, которые мы использовали, работают с Next.js из коробки. Кроме того, они поддерживают лучшие практики CSS и естественным образом вписываются в архитектуру фреймворка Next.js.
Наша организация CSS состояла из четырех ключевых частей:
- жетоны дизайна,
- Глобальные стили,
- Полезные классы,
- Стили компонентов.
По мере того, как мы продолжаем создавать наш сайт, наш список токенов дизайна и служебных классов будет расти. Любые стили, которые не имеет смысла добавлять как служебный класс, мы можем добавить в стили компонентов с помощью модулей CSS. В результате мы можем найти непрерывный баланс между локальными и глобальными проблемами стиля. Мы также можем создавать эффективный, интуитивно понятный код CSS , который естественным образом растет вместе с нашим сайтом Next.js.