После взлёта тайпскрипта (извини, flow) нетипизированные области фронтенда стали мозолить глаза гораздо сильнее. Логика уже давно на TS, вёрстка, при необходимости, на TSX, а вот у CSS ситуация посложнее.
Можешь использовать CSS файлы (с диалектами и модулями по вкусу) и указывать классы в вёрстке руками - но типизация здесь на уровне "препроцессор может сгенерировать тайпинги со списком классов прямо в рабочем дереве", да и в общем интеграция с рантаймом никакая. При этом гибкость диалекта достигается с помощью плагинов - которые, в общем случае, друг с другом (и, тем более, с IDE) могут и не дружить.
Либо бери любое из CSS-in-JS решений, предоставляющих полную типизацию и интеграцию с остальным кодом, но готовься платить ощутимое пенальти в рантайме - всё же парсинг объектов/строк со стилями занимает ощутимое время. Гибкость при этом, разумеется, максимальная.
Где-то в промежутке между ними находятся проекты вроде linaria или astroturf, которые предполагают парсинг CSS-in-JS на этапе компиляции (примерно как с graphql-tag). Типизация на уровне, производительность в рантайме - тоже, однако это всё ещё препроцессоры строк, пусть и более умные, так что расширяемость оставляет желать лучшего.
Вот тут-то в дело и вступает vanilla-extract. Пару месяцев назад Mark Dalgleish (один из создателей CSS модулей, кстати) решил узнать что получится, если использовать для препроцессинга стилей... сам тайпскрипт!
Спойлер: получилось очень хорошо. Впрочем, обо всём по порядку.
Пример использования
И сразу код (поиграться можно в codesandbox).
Почему не под спойлером?
Я бы, может, и засунул код под спойлер, да только вот редактор хабра наглухо виснет при попытке это сделать.
//App.css.ts
//весь этот файл скомпилируется в нативный css
//и маленькую js обвязку с экспортами!
import {
createTheme,
style,
globalStyle,
composeStyles
} from "@vanilla-extract/css";
//а это уже third-party библиотека
//версия classnames, заточенная под специфику vanilla-extract
import { vcn } from "vanilla-classnames";
//создаём тему и её "контракт"
export const [lightTheme, vars] = createTheme({
color: {
body: "white",
text: "black",
inactive: "gray",
active: "red"
}
});
//добавляем новую тему, используя контракт из предыдущей
export const darkTheme = createTheme(vars, {
color: {
body: "black",
text: "white",
inactive: "gray",
active: "blue"
}
});
//глобальный стиль
globalStyle("body", {
//это будет "ссылкой" на переменную
//background-color: var(--color-body__1bu5mlq1);
backgroundColor: vars.color.body
});
//обычный scoped стиль
export const switchButton = style({
marginBottom: "20px"
});
//scoped стиль
const common = style({
display: "flex",
justifyContent: "center",
alignItems: "center",
userSelect: "none",
cursor: "pointer",
color: vars.color.text
});
const size = style({
width: "150px",
height: "150px",
transition: "width 1s, height 1s",
//сразу с дополнительным селектором
":hover": {
width: "200px",
height: "200px"
}
});
//toggle - это функция, принимающая что-то вроде
// {active: someCondition} и возвращающая строку классов
export const toggle = vcn(composeStyles(common, size), {
active: [
//стиль если переключатель активен
style({
background: vars.color.active
}),
//и если неактивен
style({
background: vars.color.inactive
})
]
});
//App.tsx
import React, { useEffect } from "react";
//импорт из предыдущего файла
import * as S from "./App.css";
//функции для работы с темами
function cleanThemes() {
document.body.classList.remove(S.lightTheme);
document.body.classList.remove(S.darkTheme);
}
function light() {
cleanThemes();
document.body.classList.add(S.lightTheme);
}
function dark() {
cleanThemes();
document.body.classList.add(S.darkTheme);
}
function switchTheme() {
if (document.body.classList.contains(S.lightTheme)) dark();
else light();
}
//наш react компонент. Можно было бы и document.write использовать при желании
export const App = () => {
const [active, setActive] = React.useState(false);
useEffect(() => {
light();
return cleanThemes;
}, []);
return (
<>
<button className={S.switchButton} onClick={switchTheme}>
Switch theme
</button>
<div
//компактненько, да?
className={S.toggle({ active })}
onClick={() => setActive((r) => !r)}
>
<h2>Click me!</h2>
</div>
</>
);
};
А теперь, имея пример перед глазами, можно уже и обсудить различные нюансы.
Предупреждение
Начнём с того, что сейчас (май 2021) это альфа-версия (v0.4.3). Какие-то API могут ещё меняться, интеграции работать криво, а крайние случаи - не отрабатываться. В общем, классический набор для новых технологий.
Впрочем, разработчики говорят (но не обещают) что будут готовы к стабильной версии в течении нескольких недель, а это значит что пробовать на пет-прожектах можно уже сейчас.
Пока в наличии есть плагины для webpack, snowpack, gatsby, esbuild и vite (последний ещё сырой). Ну и есть плагин для babel, упрощающий отладку стилей - он добавляет человекочитаемые названия в сгенерированные классы.
Однако, даже в таком виде проект буквально за пару месяцев уже набрал две тысячи звёзд на гитхабе (и, надеюсь, хабр подкинет парням ещё, они того заслуживают).
Как работает?
Механизм работы не слишком-то и сложен. Плагин для бандлера использует внутренний пакет для интеграций, который и делает всю полезную работу - прогоняет .css.ts файл через esbuild (который умеет переваривать тайпскрипт), а потом запускает получившийся код как модуль ноды.
Все вызовы style
и прочих подобных функций добавляют стили в css-строку, а экспорты из модуля преобразуются в обычный js-файл, который вместе с css возвращается в бандлер.
//было
export const switchButton = style({
marginBottom: "20px"
});
//стало в js
export var switchButton = 'App-switchButton-some-hash'
//в css
.App-switchButton-some-hash {
margin-bottom: 20px;
}
Единственный неординарный трюк заключается в специальной обработке экспортируемых функций. vanilla-extract требует чтобы на каждой из них висело специальное свойство __recipe__
в котором описывается как создать эту функцию в рантайме:
//было
export function fn() {}
fn.__recipe__ = {
importPath: 'some-library',
importName: 'createFn',
args: [1, 2, 3] //аргументы должны быть сериализуемы
}
//стало
import {createFn} from 'some-library'
export var fn = createFn(1, 2, 3)
Эта фича предназначена в основном для авторов библиотек и она открывает абсолютно новые возможности по интеграции JS и CSS, прокидывая мост между билдтаймом и рантаймом.
А ещё она даже не задокументирована и может измениться :)
Конечно, с таким подходом придётся держать в уме, в каком контексте будет исполняться код, чтобы случайно не импортировать из билдтайм-файла рантайм-файл, но это выглядит как несложная задача для любого линтера.
Предшественник и особенности
На самом деле vanilla-extract - это развитие проекта treat от тех же авторов. treat основан на таком же принципе, но с несколькими значимыми ограничениями: .treat.ts файлы, которые исполняются в момент билда, не могут импортировать друг друга и нет возможности экспортировать из них функции (что очень важно, как я подчёркивал выше).
Но самое главное отличие - реализация тем. В treat они компилируются в статические классы вида .lightTheme_someClass
и для использования требуют рантайм-утилиту, которая из текущей темы и класса создаст нужную строку.
В vanilla-extract же для тем используются нативные CSS переменные, что скидывает весь рантайм на браузер и позволяет свести все необходимые манипуляции к установке класса темы на корневой элемент приложения.
При этом, конечно, их использования можно избежать - например, статически генерировать все возможные варианты правил с темами, как в treat, или там использовать фоллбек на стандартную тему и в рантайме выключать возможность сменить её если браузер IE не поддерживает CSS переменные. Учитывая, что в качестве препроцессора у нас целый джаваскрипт, придумать можно очень многое.
Кстати, vanilla-extract слегка opinionated - дополнительные селекторы локального стиля должны быть нацелены на элемент с этим стилем.
const someClass = style({
background: 'green',
selectors: {
//вот так норм
'.dark &': {
background: 'blue'
}
//и так
'&:hover': {
color: 'white'
}
//а вот такой селектор нельзя
//ссылается на дочерние элементы
'& a': {
color: 'red'
},
}
})
//а так получится, смысл тот же что в запрещённом селекторе
//но явно видно что это глобальный стиль
globalStyle(`${someClass} > a`, {
color: 'red'
})
Смысл этого искусственного ограничения прост - если один локальный класс влияет на другой локальный класс, то всегда точно известно что это правило будет в том классе, на который влияют. Ну а для влияния на глобальные классы или, тем более, на теги, предназначен globalStyle
Кстати, вот эти строки селекторов никак не типизированы и даже псевдо-селекторы на корректность не проверяются, но это как раз должно быть исправимо с помощью дополнительных утилит, ждём библиотек - ну, или пишем сами?..
Кстати, про экосистему
В монорепе vanilla-extract живёт несколько дополнительных пакетов. И если про несколько вспомогательных функций для работы с переменными и темами в рантайме говорить особо нечего, то вот sprinkles - это готовый фреймворк для построения своего атомарного CSS.
Выглядит оно крайне интересно, хотя с "традиционным" атомарным CSS различается в очень важном нюансе - получающиеся атомы не становятся глобальными классами (пока), так что использовать их напрямую в разметке не получится. Зато остальные преимущества на месте - стилей должно получиться намного меньше по объёму, можно "вшить" принятые на проекте дизайн-соглашения, брекпоинты и всякие удобные сокращения для часто используемых совместно свойств.
На основе sprinkes уже сделали компонент Box
для реакта - dessert-box
Кстати, библиотека vanilla-classnames, которую привёл в примере - это уже моя разработка, такой вот shameless plug. Подкиньте звёзд, коль не жалко ;)
Немного философии напоследок
Использование JS как препроцессора открывает множество возможностей для расширения - в отличии от обычных препроцессоров с системой плагинов, кастомным AST и прочим, тут достаточно написать обычную функцию и потом вызвать её, напрямую или опосредованно.
При этом всё будет типизировано и по необходимости связано с рантаймом сколь угодно сложным образом. Аналог styled-components возможно сделать за десять минут, сохранив все их преимущества и ничего не потеряв в производительности.
В целом, сам подход vanilla-extract - это достаточно новая техника (как минимум для экосистемы TS). Это не назвать традиционным компилятором и даже препроцессором - оно никак не завязано на AST. Больше, наверно, похоже на макросы из того же Rust, хоть и со своей спецификой.
Учитывая гибкость этого подхода и огромную интероперабельность между css и js в нём, осталось лишь подождать когда на его основе наделают множество утилит (или даже целых DSL), которые закроют болевые точки, о которых мы и не подозреваем, пока не увидим их решение.
Как вам, например, строго типизированный z-index (и вообще контекст наложения) во всём приложении? Или возможность рантайм проверок правильной вложенности классов без единой дополнительной строчки кода? Или же вообще полностью новая система стилизации, предназначенная именно для приложений, а не документов, но при этом компилируемая в нативный css и работающая с его скоростью, да ещё и учитывающая современные компонентные подходы? И это всё без нового синтаксиса, замечу, и не влияющая на производительность в рантайме.
Поживём - увидим.
monochromer
А стили точно типизированные?
Amareis Автор
Хороший пример, точно так же как и со строками селекторов, единицы измерения не более типизированы чем в самом CSS, так что улучшать есть ещё куда.
monochromer
Работы уже ведутся (доступно в Chrome):
faiwer
Не очень понимаю зачем вообще в этом типе есть хоть что-то кроме
string |
, при условии чтоstring |
уже есть. И какой толк от таких стилей? Типа я не могу сюдаboolean
поставить? В этом суть? :)Вот тут вот была попытка сделать типизированные стили. А в чём смысл
vanilla-extract
я так и не понял.Amareis Автор
Ну в общем-то, тот подход можно переложить на vanilla-extract без особого труда. Я бы рассматривал его как высокоуровневый генератор CSS, который запускается в билдтайме и может быть связан с рантаймом. Рассматривайте его как любой препроцессор CSS, только вместо кастомного диалекта у вас тайпскрипт, а вместо плагинов — обычные функции, которые вы сами применяете по мере необходимости.
faiwer
Ну т.е. всё то что мы не любим в css-in-js :) А каковы преимущества?
Amareis Автор
Ну не знаю как вы, а мне вполне нравится. Из преимуществ — максимальная гибкость, при этом никакой расплаты за неё в рантайме.
asakasinsky
В опросе не хватает «Никогда не хотел».
CBNHYIIIOK1
Какая галиматья, как и весь этот css-in-js.
DmitryKazakov8
Скорей бы это смешение стилей и логики ушло в историю. Вот смешение разметки и логики (JSX) - это удобно, так как они очень тесно связаны, а стили - совсем другой слой и программе все равно есть они или нет, без них все будет работать так же, это чисто визуальная составляющая.
Я уже со счета сбился в перечислении недостатков, которые css-in-js вносит в проект, по сравнению с модулями. И тем не менее иногда встречаются проекты, использующие этот нежизнеспособный концепт, запутываясь в сотнях одинаковых с лица но кардинально разных компонентах Button / StyledButton (в лучшем случае), тоннах omitProps, смешении стилей и компонентов, ужаснейше выглядящих и не поддающихся форматированию template-вставках, постоянных "> * {}" для переписывания стилей любых чайлдов, и список этот настолько длинен, что на вот такую статью как наверху потянет, чисто перечисление...
MetromDouble
А почему оно должно уйти? Всё связано и взаимодействует со всем. Скриптам в браузере необходимо эффективно и гибко менять как объекты в памяти, так и объекты в разметке и стилях. CSS-модули конечно могут предоставить управление стилями при помощи классов для большинства случаев, но вам не кажется, что как раз это и есть костыль?
Современные браузеры унаследовали слишком много черт того времени, когда интернетом владели веб-страницы. Для доминирующих сейчас веб-приложений по-хорошему нужны браузеры, основанные совсем на других принципах, а не DOM/CSSOM/JS. Скорее WASM+WEBGPU
DmitryKazakov8
Должно уйти, потому что многословно, неудобно, низкопроизводительно и приводит к смешению концептуально разных сущностей и языков. Повторюсь, у всех реализаций при применении на практике громадное количество костылей и недостатков.
Управление стилями при помощи добавления-удаления классов очень эффективно, так как позволяет за раз переключать сразу несколько параметров и переиспользовать эти классы в других компонентах. Схема через template literals
${isActive => isActive ? 'margin: 0; color: red;' : 'margin: 1px; color: green;'}
как раз выглядит хаком, а не&.active {margin: 0; color: red;}
.В целом не могу сказать, что что-то в отдельном CSS меня не устраивает — это долго развивавшийся язык, с отличной поддержкой в IDE, многочисленными инструментами для добавления функционала через пре- и пост-процессинг, линтерами/форматтерами. Часто говорят, что css-in-jss это мощно, потому что можно написать какие угодно инструменты на js — но они и так есть для CSS, и тотальное переписывание, чтобы было так же удобно работать, как и с CSS, приведет к тому, что получится то же самое. Просто файлы не с расширением .scss, а .ts и дополнительным оверхедом по синтаксису, экспортам и торможению основной сборки. Но для этого "светлого будущего" нужно еще написать сотни инструментов, исправить тысячи багов, интегрироваться во все IDE, стандартизировать параметры и значения (а текущий TS этого не позволяет в должной мере, string и все тут). В то время как все давно уже есть, и ради маленькой фичи "не хочу писать
cn(styles.class1, isActive && styles.class2)
, хочу писатьclass1({ isActive })
и логику внедрять внутрь стилей, потому что так смогу все размазать в единый слой" все эти усилия… Стоит ли оно того, или пора все же в историю?Amareis Автор
В целом это можно долго обсуждать, но хочу заметить что "маленькая фича" в виде более удобной композиции класснеймов, это только первый предвестник тех фич, которые можно сделать, в том числе и самому, без всякой расплаты в рантайме. Вопрос здесь только в том, как скоро эти отдельные маленькие удобства накопятся до состояния, когда нативный CSS станет просто целью для компиляции, как это уже произошло с HTML. Посмотрите на тот же sprinkles, который позволяет сделать DSL для стилей, заточенный под конкретно вашу дизайн-систему.
А по поводу производительности — это очень сложный вопрос. Лично мне кажется что используемая схема "убрали типы и запустили сразу в ноде" может оказаться быстрее препроцессоров традиционных диалектов.
Про оверхед по синтаксису же… Ну, если стремиться к сходству с обычным CSS, то безусловно оверхед будет. Если же делать на его основании DSL, то уже CSS будет выглядеть вербозным.
DmitryKazakov8
При единых выходных файлах действительно расплата будет не в рантайме, но в билдтайме. Стилевые TS файлы нагружают парсер IDE и общий лоадер для ts-файлов, что приведет к деградации скорости сборки, поиска по проекту, подсветки типов, линтера. Конечно, работа с CSS тоже не бесплатна, но она параллелится, а тут все в большой куче получается.
Sprinkles посмотрел — это переизобретенные mixins, в которых давно реализовано то же самое:
Этого инструментария хватает для того, чтобы сделать приложение любой сложности и вполне удобно контролируется.
"Более удобная композиция класснеймов" — спорно, я не вижу ни одной неудобной строчки в таком варианте:
В CSS файле все удобно разложено и красиво отформатировано. Приоритет стилей определяется порядком их следования в стилевом файле и усиленной специфичностью. В css-in-js в большинстве реализаций описание подобного превратилось бы в ад, как и стилевой файл.
По поводу вербозности — каким бы ни был лаконичным DSL, он максимум что сможет сделать — приблизиться к композиционному подходу с миксинами, при этом сохранятся все вот эти обертки типа
createAtomicStyles, createAtomsFn
, экспорты, нетипизированные значения без подсказок от IDE и соответственно с большой вероятностью опечаток, кастомное структурирование типаconditions, defaultCondition, properties
. Все это абсолютно лишний бойлерплейт — через устоявшиеся подходы все делается удобней и не нужно никакого дополнительного обучения для разработчиков, достаточно изучить файлы с константами и миксинами при переходе в новый проект.Amareis Автор
Как и CSS файлы, так что особой разницы не вижу. Понятно что те могут быть побыстрее, но с учётом того, что на экране редко бывает больше двух-трёх файлов — вообще неважно.
Как раз вот нет, учитывая что vanilla-extract использует esbuild в фоне — эти стилевые файлы обрабатываются параллельно в отдельном лоадере, тут разницы с CSS особой нет.
Разве что полезную работу производит конкретно написанный вами код, а не дженерик компилятор, обходящий AST написанного вами кода. Разница в скорости пока не ясна (надо бы попроводить замеры), но, умозрительно, есть вероятность что так окажется даже побыстрее.
Есть важное отличие — sprinkles это атомарный CSS, так что результирующие стили будут состоять, условно, из пары килобайт атомарных классов, при том что вёрстка от этого вообще никак не изменится. Как вам такая оптимизация на ровном месте?
Я вижу, в основном, в том, что стили которые по замыслу должны работать вместе, в самом коде CSS никак не связаны. Надеюсь, важность инкапсуляции вы отрицать не будете? Условный BEM решает эту проблему через строгие правила именования, а тут она решается бесплатно, самым органичным (чистая функция) и наименее вербозным образом.
Тут опять же есть крайне важное отличие — DSL того же sprinkles абсолютно типизирован, так что вам даже не нужно изучать константы и миксины, всё находится на кончиках пальцев — вернее, в окошке автодополнения редактора.
А бойлерплейт не особо и отличается от тех самых файлов с миксинами и константами, так что тут различия не вижу.
Ну это уже наследственная болезнь CSS — да и то, проверять значения в билд-тайме (а при пущем желании и в компайл-тайме, с учетом template types в TS 4.1) — это дело нескольких дополнительных строк кода.
DmitryKazakov8
По поводу атомарных классов — в препроцессорах можно включить extend, либо просто прогонять результирующие файлы через CSSO — сомневаюсь, что будут отличия в размерах, либо в моем варианте все же будет получше, потому что не руками оптимизируется, а автоматически:
По скорости билда если и не будут различий, все равно создается дополнительная нагрузка на IDE и тайпчекинг — IDE включит ваши файлы в индекс и так или иначе часто будет их процессить даже при закрытых вкладках. Тайпчекинг тоже замедлится и в целом непонятно, зачем он — разве что для нахождения несуществующих классов, чтобы было удобней рефакторить. Единственный плюс, пожалуй.
То, что все эти кастомные структуры будут в автоподсказках не значит, что их не требуется изучать и что они вообще нужны.
Мы очень сузили в обсуждении темы для сравнения, надо было видимо написать пятисотстрочник с недостатками css-in-js, чтобы было больше материала) А по обсуждаемым пунктам — да, ваш вариант где-то на горизонте видит экосистему CSS и изо всех сил бежит к ней, в чем-то сравниваясь по скорости, в чем-то по удобству. Но чтобы догнать нужны совсем другие усилия и намного более основательные причины, чем "можно тайпчекать наличие классов и писать js-логику прямо в стилях", при этом с кучей недостатков.
Чуть не забыл сказать про инкапсуляцию:
Пожалуйста — nested структура, внутренние классы работают только если заключены в обертку
.class
. Структура вложенности для удобства должна отражать html-структуру разметки, чтобы при изменении стилей можно было практически не лезть в файлы с разметкой. Добавляем модули = изоляция, инкапсуляция. И ни одногоcreateAtomicStyles, createAtomsFn, export
и т.п.CBNHYIIIOK1
Как, кому, почему, в результате приема каких веществ в голове могла возникнуть мысль о том что это проблема???
Или у вас просто руки чешутся протипизировать что угодно? Тогда вам к психиатру, или просто загрузиться нормальной работой.
dom1n1k
По моему, вся тема CSS-in-JS — это идеальное олицетворение пословицы про молоток и гвозди.
А то что в статье — это не венец, это апофеоз.
bayarsaikhan
Я написал ниже аналогичный комент про молоток и гвозди еще не видя ваш коммент. Совпадение какое :)
jakobz
Я вот наелся этого - аж целый фреймворк был на css-in-js. И вернулся в css modules с sass. По двум причинам:
офигеешь разбираться что сам навернул
тупо нельзя скопировать в код, то что накрутил в инспекторе
RiverFlow
Каменты прям вдохнули жизненной уверенности, что не все так плохо на этой планетке)
Типизация это шаг назад на те же грабли а аргументы "это же костыль" - маркер кодера-инвалида "архитектурнутого" на всю голову типизацией и "чистотой кода" не умеющего написать ничего дельного но умеющего объяснить начальству почему все так сложно и ему нужно срочно джунов нанять а лучше быстрее сделать его лидом и мидлов ему в упряжку и вот тогдаааа....
Он займётся мозгоипанием своим ООП всех до кого дотянется и скорбно будет "так-и-быть-соглашаться" с "этими-вашими-костылями" ибо дедлайн а "как-надо", вы мартышки нивтащили, сколько я вам ни объяснял!
Трагедия js в том, что джава-какашата, похоронив таки свою джаву не раскаялись а просто сменили цель и сделали js следующей жертвой своих устремлений по идеологизации несовершенного мира который "мозолит глаза своей нетипизированностью" и долбят теперь своими сферическими конями мозг неофита и инвесторам ...
Amareis Автор
Просто те, кому всё понравилось, молча добавили в закладки и поставили звезду на гитхабе ;) И таковых в два раза больше чем комментов.
В остальном даже вступать в полемику не буду, если вы всё ещё уверены в том, что отсутствие статической типизации — это всегда благо, то тут даже говорить не о чем.
CBNHYIIIOK1
Ну ясно, «одна я вся в белом пальто стою».
Над вашей ахинеей смеются, а не полемизируют по причине отсутствия предмета для серьезного обсуждения и проблематики высосаной из пальца, см. выше.
DmitryKazakov8
Неожиданный сайд-эффект: по запросу в Яндексе "какашата" данная статья вылезает на 3 позиции. Может, эти лиды ставят звезды на гитхабе и в закладки сохраняют…
P.S. Извини автор за такие шутки, но видеть в проектах css-in-js это такая боль. Почему-то тысячи стилевых багов мне встречались исключительно в таких проектах.
bayarsaikhan
У меня ощущение последние несколько лет, что мы (программисты, любители типизаций и т. д.), будучи молотком, во всем видим гвоздь. Вот в данном случае мы пытаемся типизировать то, парадигма чего лежит полностью за пределами области программирования. Можно попытаться типизировать типографию, например, но что мы, собственно этим решим? Не тупиковый ли это путь? Может решения лежат в совершенно другом направлении (если вообще есть проблема)?
P.S. Только что увидел аналогичный комент про «молоток и гвозди» выше.
JTG
Какое же безумие творится на фронте.