[Перевод] Atomic CSS здорового человека
Перевод статьи «Reimagine Atomic CSS» двухлетней давности одного из членов команды Vue core Anthony Fu, автора UnoCSS, в которой обсуждается концепция Atomic CSS, плюсы и минусы Tailwind и Windi
Первая часть
Что такое Atomic CSS?
Для начала давайте дадим правильное определение атомарному CSS:
Из этой статьи Джона Полачека:
Атомарный CSS — это подход к архитектуре CSS, при котором предпочтение отдается небольшим, одноцелевым классам с именами, основанными на визуальной функции.
Некоторые также могут называть его функциональным CSS, или CSS-утилитами. В принципе, можно сказать, что фреймворк Atomic CSS — это набор таких CSS, как эти:
.m-0 {
margin: 0;
}
.text-red {
color: red;
}
/* ... */
У нас есть довольно много CSS-фреймворков, основанных на утилитах, таких как Tailwind CSS, Windi CSS и Tachyons, и т.д. Также есть несколько библиотек пользовательского интерфейса, которые поставляются с некоторыми CSS-утилитами в качестве дополнения к фреймворку, например Bootstrap и Chakra UI.
Мы не будем говорить здесь о плюсах и минусах использования атомарного CSS, поскольку вы уже много раз слышали об этом. Сегодня мы воспользуемся точкой зрения автора фреймворка и посмотрим, как мы находим компромисс при создании этих любимых вами фреймворков, каковы их ограничения и что мы можем сделать лучше, чтобы в конечном итоге принести пользу вашей повседневной работе.
Предыстория
Прежде чем мы начнем, давайте поговорим немного о предыстории. Если вы меня не знаете, меня зовут Энтони Фу, и я являюсь членом команды Vite и создателем Vitesse, одного из самых популярных стартовых шаблонов для Vite. Мне нравится скорость разработки на основе атомарного CSS (или CSS-утилит), поэтому я решил использовать Tailwind CSS в качестве UI-фреймворка по умолчанию для Vitesse. Хотя Vite должен быть невероятно быстрым по сравнению с Webpack и другими, Tailwind, который генерирует мегабайты утилит CSS, делает запуск и HMR на Vite медленным, как в старые добрые времена. Когда-то я думал, что это своего рода компромисс за использование атомарных CSS-решений — пока не обнаружил Windi CSS.
Windi CSS — это альтернатива Tailwind CSS, которая была написана с нуля. Он имеет нулевые зависимости и не полагается на PostCSS и Autoprefixer. Что еще более важно, в нем реализовано потребительское использование. Вместо того чтобы генерировать все комбинации утилит, которые вы редко используете, чтобы потом вычистить их, Windi CSS генерирует только те, которые действительно представлены в вашей кодовой базе. Это отлично вписывается в философию Vite, основанную на использовании по требованию, и теоретически должно быть намного быстрее, чем Tailwind. Поэтому я написал плагин Vite для него, и оказалось, что он в 20–100 раз быстрее, чем Tailwind.
Все шло довольно хорошо, Windi CSS выросла в команду, мы сделали еще много нововведений, таких как Value Infering, Variant Groups, Shortcuts, Design in DevTools, Attributify Mode и т.д. В результате Tailwind получил пинок под зад, чтобы представить свой собственный движок по требованию JIT.
Исследование Atomic CSS
Возвращаясь к теме, давайте сначала рассмотрим, как работает атомарный CSS.
Традиционный способ
Традиционный способ создания Atomic CSS заключается в предоставлении всех утилит CSS, которые вам могут понадобиться. Например, вот то, что вы можете сгенерировать самостоятельно с помощью препроцессора (в данном случае SCSS):
// style.scss
@for $i from 1 through 10 {
.m-#{$i} {
margin: $i / 4 rem;
}
}
Это будет скомпилировано в:
.m-1 { margin: 0.25 rem; }
.m-2 { margin: 0.5 rem; }
/* ... */
.m-10 { margin: 2.5 rem; }
Здорово, теперь вы можете использовать class="m-1"
для установки поля. Но, как вы понимаете, при таком подходе вы не сможете задать отступ от 1 до 10, а также вам придется заплатить за доставку 10 правил CSS, даже если вы использовали только одно. Позже, если вы захотите поддерживать различные направления отступов, например mt
для margin-top
, mb
для margin-bottom
. С этими 4 направлениями вы умножаете размер CSS на 5. А когда дело доходит до таких вариантов, как hover:
и focus:
— вы знаете, что происходит. На этом этапе добавление еще одной утилиты часто означает, что вы введете несколько дополнительных килобайт. Именно поэтому традиционный Tailwind поставляется с мегабайтами CSS.
Чтобы решить эту проблему, Tailwind придумал решение с помощью PurgeCSS для сканирования вашего пакета dist и удаления ненужных правил. Теперь в продакшене осталось всего несколько килобайт CSS. Однако обратите внимание, что очистка будет работать только в производственной сборке, то есть вы все еще будете работать с огромным количеством CSS в разработке. В Webpack это было не так заметно, но в Vite это становится проблемой, учитывая, что все остальное происходит молниеносно.
Хотя подход к генерации и очистке имеет свои ограничения, можно ли найти лучшее решение?
On-demand решение
Идея «по требованию» представляет собой совершенно новый способ мышления. Давайте проведем сравнение подходов.
Традиционный способ не только стоит вам лишних вычислений (созданных, но не используемых), но и не способен удовлетворить ваши потребности, которые не учитываются в первую очередь.
Поменяв местами порядок «генерации» и «сканирования использования», подход «по требованию» позволяет сэкономить на вычислениях и передаче данных, а также гибко реагировать на динамические потребности, которые предварительная генерация удовлетворить не может. Между тем, этот подход можно использовать как в разработке, так и в производстве, обеспечивая большую уверенность в согласованности и повышая эффективность HMR.
Для достижения этой цели и Windi CSS, и Tailwind JIT используют подход предварительного сканирования исходного кода. Вот простой пример этого:
import { promises as fs } from 'node:fs'
import glob from 'fast-glob'
// this usually comes from user config
const include = ['src/**/*.{jsx,tsx,vue,html}']
async function scan() {
const files = await glob(include)
for (const file of files) {
const content = await fs.readFile(file, 'utf8')
// pass the content to the generator and match for class usages
}
}
await scan()
// scanning is done before the build / dev process
await buildOrStartDevServer()
Чтобы обеспечить HMR во время разработки, обычно требуется наблюдатель за файлами:
import chokidar from 'chokidar'
chokidar.watch(include).on('change', (event, path) => {
// read the file again
const content = await fs.readFile(file, 'utf8')
// pass the content to the generator again
// invalidate the css module and send HMR event
})
В результате, благодаря подходу «по требованию», Windi CSS способен обеспечить примерно 100-кратное ускорение по сравнению с традиционным Tailwind CSS.
Недостатки
Сейчас я использую Windi CSS почти во всех своих приложениях, и он работает довольно хорошо. Производительность отличная, а HMR незаметен. Автоинферинг значений и Режим атрибутирования делают мою разработку еще быстрее. Тогда я действительно могу хорошо выспаться и помечтать о других вещах. Правда, иногда меня мучает зуд от сладкого сна.
То, что меня раздражает, — это неясность того, что я получаю и что нужно делать, чтобы это работало. На мой взгляд, в идеале атомный CSS должен быть невидимым. После изучения он должен быть интуитивно понятен и аналогичен остальным. Он невидим, когда работает так, как вы ожидаете, и может разочаровать, когда это не так.
Например, вы знаете, что в Tailwind’е border-2
означает 2px
ширины границы, 4
для 4px
, 6
для 6px
, 8
для 8px
, но знаете что, border-10
не работает (чтобы понять это, вам может понадобиться время!). Вы можете сказать, что это специально сделано Tailwind, чтобы сделать систему дизайна последовательной и ограниченной. Хорошо, но вот вам быстрый тест: Допустим, если вы хотите, чтобы border-10
работал, как вы это сделаете?
Написать собственную утилиту где-нибудь в глобальных стилях?
.border-10 {
border-width: 10px;
}
Это довольно быстро и просто. И что важно, это работает. Но, честно говоря, если мне нужно делать это вручную, зачем мне вообще нужен Tailwind?
Если вы знакомы с Tailwind немного больше, вы можете знать, что его можно настраивать. Итак, вы потратили 5 минут на поиск их документации, и вот что у вас получилось в итоге:
// tailwind.config.js
module.exports = {
theme: {
borderWidth: {
DEFAULT: '1px',
0: '0',
2: '2px',
3: '3px',
4: '4px',
6: '6px',
8: '8px',
10: '10px' // <-- here
}
}
}
Ах, справедливо, теперь мы могли бы перечислить их все и вернуться к работе… подождите, на чем я остановился? Первоначальная задача, над которой вы работаете, теряется, и требуется время, чтобы снова вернуться к контексту. Позже, если мы захотим установить цвета границ, нам придется снова искать документацию, чтобы понять, как это можно настроить, и так далее. Может быть, кому-то такая схема работы и понравится, но мне она не подходит. Мне не нравится, когда меня прерывают в том, что должно работать интуитивно.
Windi CSS более спокойно относится к правилам и старается предоставить соответствующие утилиты, когда это возможно. В предыдущем случае border-10
будет работать на Windi из коробки (спасибо!). Но из-за того, что Windi совместим с Tailwind, он также должен использовать точно такой же интерфейс настройки, как и Tailwind. Хотя вывод чисел работает в Windi, это все равно будет кошмаром, если вы захотите добавить пользовательские утилиты. Вот пример из документации Tailwind:
// tailwind.config.js
const _ = require('lodash')
const plugin = require('tailwindcss/plugin')
module.exports = {
theme: {
rotate: {
'1/4': '90deg',
'1/2': '180deg',
'3/4': '270deg',
}
},
plugins: [
plugin(({ addUtilities, theme, e }) => {
const rotateUtilities = _.map(theme('rotate'), (value, key) => {
return {
[`.${e(`rotate-${key}`)}`]: {
transform: `rotate(${value})`
}
}
})
addUtilities(rotateUtilities)
})
]
}
Только для того, чтобы сгенерировать эти:
.rotate-1\/4 {
transform: rotate(90deg);
}
.rotate-1\/2 {
transform: rotate(180deg);
}
.rotate-3\/4 {
transform: rotate(270deg);
}
Код для генерации CSS еще длиннее, чем результат. Его трудно читать и поддерживать, а кроме того, он нарушает возможность работы по требованию.
Система API и плагинов Tailwind разработана с учетом традиционного мышления и не совсем соответствует новому подходу «по требованию». Основные утилиты заложены в генератор, а возможности кастомизации весьма ограничены. Поэтому мне стало интересно, если мы откажемся от этих долгов и переработаем систему с нуля, ориентируясь на подход «по требованию», что мы получим?
Конец первой части. Вторая часть — Введение в UnoCSS, — в процессе перевода.