Atomic CSS здорового человека
- суббота, 2 декабря 2023 г. в 00:00:21
Перевод статьи «Reimagine Atomic CSS» двухлетней давности одного из членов команды Vue core Anthony Fu, автора UnoCSS, в которой обсуждается концепция Atomic CSS, плюсы и минусы Tailwind и Windi.
Первая часть.
Для начала давайте дадим правильное определение атомарному 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.
Возвращаясь к теме, давайте сначала рассмотрим, как работает атомарный 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 это становится проблемой, учитывая, что все остальное происходит молниеносно.
Хотя подход к генерации и очистке имеет свои ограничения, можно ли найти лучшее решение?
Идея «по требованию» представляет собой совершенно новый способ мышления. Давайте проведем сравнение подходов.
Традиционный способ не только стоит вам лишних вычислений (созданных, но не используемых), но и не способен удовлетворить ваши потребности, которые не учитываются в первую очередь.
Поменяв местами порядок «генерации» и «сканирования использования», подход «по требованию» позволяет сэкономить на вычислениях и передаче данных, а также гибко реагировать на динамические потребности, которые предварительная генерация удовлетворить не может. Между тем, этот подход можно использовать как в разработке, так и в производстве, обеспечивая большую уверенность в согласованности и повышая эффективность 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 незаметен. Value Auto Infering и Attributify Mode делают мою разработку еще быстрее. Тогда я действительно могу хорошо выспаться и помечтать о других вещах. Правда, иногда меня мучает зуд от сладкого сна.
То, что меня раздражает, — это неясность того, что я получаю и что нужно делать, чтобы это работало. На мой взгляд, в идеале атомный 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, — в процессе перевода.
Узнать интересную и полезную информацию о Vue.js, а также изучить фреймворк по переводу лучшего учебника по данной теме «Vue.js 3 — Шаблоны проектирования и Лучшие практики» можно на нашем сайте: Vue‑FAQ.org.
Также заходите на наш Телеграм‑канал: https://t.me/vuefaq