[Перевод] 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.

b5cda9099d7e447e3fca663af6be68c1.png

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 решение

Идея «по требованию» представляет собой совершенно новый способ мышления. Давайте проведем сравнение подходов.

404ba52402c870fb5f419e4df9fc59a6.png

Традиционный способ не только стоит вам лишних вычислений (созданных, но не используемых), но и не способен удовлетворить ваши потребности, которые не учитываются в первую очередь.

7dd799cb4d50015f0d6409159c62ecaa.png

Поменяв местами порядок «генерации» и «сканирования использования», подход «по требованию» позволяет сэкономить на вычислениях и передаче данных, а также гибко реагировать на динамические потребности, которые предварительная генерация удовлетворить не может. Между тем, этот подход можно использовать как в разработке, так и в производстве, обеспечивая большую уверенность в согласованности и повышая эффективность 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, — в процессе перевода.

© Habrahabr.ru