Русский
Русский
English
Статистика
Реклама

Css-in-js

Перевод Сравнение производительности CSS и CSS-in-JS в реальном мире

15.05.2021 18:08:01 | Автор: admin
Технология CSS-in-JS заняла прочное место среди инструментов фронтенд-разработки. И возникает ощущение, что CSS-in-JS-тренд в ближайшем будущем лишь усилится. Особенно в мире React. Например, в исследовании State of CSS, проведённом в 2020 году, приняли участие 11492 человека. Лишь 14,3% из них не слышали о Styled Components (о ведущей CSS-in-JS-библиотеке). А вот пользовались этой библиотекой более 40% участников исследования.



Мне уже давно хотелось найти серьёзный материал, посвящённый сравнению производительности CSS-in-JS-библиотек, вроде Styled Components, и доброго старого CSS. Но я, к сожалению, ничего такого, вроде сравнения их производительности на реальном проекте, а не на каком-то простом наборе тестов, найти не смог. Поэтому я решил сам сделать такое сравнение. Я перевёл реальное приложение со Styled Components на Linaria, на библиотеку, которая выполняет извлечение CSS в файлы во время сборки проекта. В результате в приложении, использующем Linaria, не выполняется генерирование стилей во время работы этого приложения на компьютере пользователя.

Прежде чем мы приступим к делу хочу прояснить некоторые вещи. Я не отношу себя к людям, которые ненавидят CSS-in-JS. Я признаю то, что эта технология отличается отличным опытом разработчика (Developer Experience, DX), и то, что она обладает замечательной моделью композиции, унаследованной от React. CSS-in-JS способна дать разработчикам много хорошего (почитать об этом можно здесь). Да и я сам пользуюсь библиотекой Styled Components в нескольких собственных проектах и в проектах, над которыми мне доводилось работать. Но мне всегда было интересно знать о том, сколько пользователям веб-проектов приходится платить за те удобства, которые даёт разработчикам CSS-in-JS.

Да, если вас интересуют лишь мои выводы то вот они: не используйте CSS-in-JS с вычислением стилей во время работы программы в том случае, если вы заботитесь о скорости загрузки вашего сайта. Тут всё просто: чем меньше JavaScript-кода тем быстрее сайт. И с этим ничего особо поделать нельзя. Если же вам интересно узнать о том, как я пришёл к таким выводам продолжайте читать.

Что и как я измерял


Приложение, которое я использовал в тестах это вполне обычный React-проект. Его основа создана с помощью Create React App (CRA), в нём используется Redux и Styled Components (v5). Это достаточно большое приложение с множеством экранов, с настраиваемой панелью управления, с поддержкой тем и со многими другими возможностями. Так как оно было создано с помощью CRA оно не поддерживает серверный рендеринг, в результате всё рендерится на стороне клиента (речь идёт о B2B-приложении, в перечне требований к нему серверного рендеринга не было).

Я взял это приложение и заменил Styled Components на библиотеку Linaria, которая, как мне казалось, имеет похожий API. Я полагал, что перейти со Styled Components на Linaria будет просто. Но, на самом деле, перевод приложения на новую библиотеку стилизации потребовал определённых усилий. А именно, на то, чтобы перевести приложение на Linaria, у меня ушло два месяца. Но даже после того, как у меня получилось что-то такое, с чем уже можно было работать, переведены были лишь несколько страниц, а не всё приложение. Подозреваю, что именно поэтому никто и не проводит таких сравнений, которое решил провести я. Единственное изменение, которое я внёс в приложение, было представлено заменой одной библиотеки стилизации на другую. Всё остальное осталось нетронутым.

Для запуска различных тестов, направленных на исследование двух страниц, которые используются чаще всего, я пользовался инструментами разработчика Chrome. Я всегда запускал тесты по три раза. Представленные здесь цифры это средние показатели по трём запускам тестов. Во всех тестах я устанавливал, на вкладке Performance, значение 4x slowdown для параметра CPU и значение Slow 3G для параметра Network. Для исследования производительности я использовал отдельный профиль Chrome без каких-либо расширений.

Вот какие испытания я провёл:

  1. Анализ сетевой активности приложения (размер JS- и CSS-ресурсов, анализ используемого кода, количество запросов).
  2. Исследование производительности в Lighthouse (аудит производительности с применением мобильных предустановок).
  3. Профилирование производительности (исследование загрузки страниц и особенностей drag-and-drop-взаимодействия с ними).

Анализ сетевой активности приложения


Начнём с анализа сетевой активности приложения. Одной из сильных сторон CSS-in-JS является тот факт, что при использовании этой технологии в приложение не попадает ненужных стилей. Верно? Ну, на самом деле, это не совсем так. Когда на странице имеется лишь один активный стиль, вместе с ним могут загрузиться и ненужные стили. Но эти стили находятся не в отдельном файле, а в JS-бандле.

Вот данные, полученные при исследовании домашней страницы двух вариантов приложения. Один из них, напомню, создан с использованием Styled Components, а второй с помощью Linaria. Показатель до косой черты это размер данных, сжатых gzip, а после косой черты идёт размер несжатых данных.

Сравнение сетевых показателей домашней страницы двух вариантов приложения.

Styled Components Linaria
Общее количество запросов 11 13
Общий размер 361Кб/1,8MB 356Кб/1,8Мб
Размер CSS 2,3Кб/7,2Кб 14,7Кб/71,5Кб
Количество CSS-запросов 1 3
Размер JS 322Кб/1,8Мб 305Кб/1,7Мб
Количество JS-запросов 6 6

Сравнение сетевых показателей поисковой страницы двух вариантов приложения.

Styled Components Linaria
Общее количество запросов 10 12
Общий размер 395Кб/1,9Мб 391Кб/1,9Мб
Размер CSS 2,3Кб/7,2Кб 16,0Кб/70,0Кб
Количество CSS-запросов 1 3
Размер JS 363Кб/1,9Мб 345Кб /1,8Мб
Количество JS-запросов 6 6

Даже несмотря на то, что в Linaria-варианте приложения значительно возрос объём загружаемого CSS-кода, общий объём загружаемых данных снизился у обеих страниц (хотя в данном случае эта разница почти незаметна). Но самое важное тут то, что общий объём CSS- и JS-данных Linaria-варианта страниц меньше, чем размер JS-бандла того варианта приложения, в котором используется Styled Components.

Анализ используемого кода


Если проанализировать объём используемого кода, то окажется, что в Linaria-варианте приложения имеется большой объём (около 55 Кб) неиспользуемого CSS-кода. А в приложении, где применяется Styled Components это всего 6 Кб (причём это CSS из npm-пакета, а не из самой библиотеки Styled Components). Размер неиспользуемого JS-кода в Linaria-варианте приложения на 20 Кб меньше, чем в Styled Components-варианте. Но общий объём неиспользуемого кода больше там, где применяется Linaria. Это один из компромиссов, на которые приходится идти тому, кто использует внешний CSS.

Анализ используемого кода домашней страницы.

Styled Components Linaria
Размер неиспользуемого CSS 6,5Кб 55,6Кб
Размер неиспользуемого JS 932Кб 915Кб
Общий размер 938,5Кб 970,6Кб

Анализ используемого кода поисковой страницы.

Styled Components Linaria
Размер неиспользуемого CSS 6,3Кб 52,9Кб
Размер неиспользуемого JS 937Кб 912Кб
Общий размер 938,5Кб 970,6Кб

Аудит производительности в Lighthouse


Если уж мы говорим об анализе производительности непростительно будет не взглянуть на то, что выдаёт Lighthouse. Сравнение показателей (средние значения после трёх запусков Lighthouse) можно видеть на нижеприведённых диаграммах. Тут, помимо показателей группы Web Vitals, имеются ещё два показателя Main thread work и Execution time. Main thread work это время парсинга, компиляции и запуска ресурсов, большая часть которого уходит на работу с JS, хотя вклад в этот показатель вносят и подготовка макета страницы, и вычисление стилей, и вывод данных, и другие процессы. Execution time это время выполнения JS-кода. Я не включил сюда показатель Cumulative Layout Shift, так как он близок к нулю, и он выглядит практически одинаково для вариантов приложения, в котором используется Linaria и Styled Components.


Показатели Lighthouse для домашней страницы


Показатели Lighthouse для поисковой страницы

Как видите, Linaria-вариант приложения лучше, чем Styled Components-вариант, выглядит в Web Vitals-тестах (он показал худший результат лишь однажды, по показателю CLS). Иногда преимущество оказывается довольно-таки значительным. Например, на домашней странице показатель LCP оказывается лучше на 870 мс, а на поисковой странице на 1,2 с. Страница, на которой используется обычный CSS, не только быстрее рендерится, но и требует меньше ресурсов. А время блокировки и время, необходимое на выполнение всего JS-кода, соответственно, меньше на 300 мс и примерно на 1,3 с.

Профилирование производительности


Lighthouse может дать нам много интересных сведений о производительности приложений. Но нет лучшего средства для детального анализа производительности, чем вкладка инструментов разработчика Chrome Performance. В данном случае то, что удалось выяснить с помощью инструментов этой вкладки, согласуется с тем, что мы уже видели после выполнения Lighthouse-испытаний. Взгляните на следующие диаграммы.


Профилирование производительности домашней страницы


Профилирование производительности поисковой страницы

На страницах, при создании которых используется библиотека Styled Components, имеется больше задач, выполняющихся длительное время. И на завершение этих задач, в сравнении с Linaria-вариантом страниц, нужно больше времени.

Для того чтобы рассмотреть данные профилирования производительности под несколько иным углом, ниже я привёл совмещённые графики загрузки Styled Components-варианта домашней страницы (выше) и её Linaria-варианта (ниже).


Сравнение процесса загрузки разных вариантов домашней страницы

Сравнение особенностей drag-and-drop-взаимодействия со страницами


Я решил сравнить страницы не только по показателям их загрузки, но и на предмет их быстродействия при работе с ними. А именно, я измерил производительность страниц при выполнении действий, предусматривающих перетаскивание элементов и размещение их по группам. Итоговые результаты приведены ниже. Как видно, даже в этом тесте Linaria побеждает Styled Components в нескольких категориях.

Styled Components Linaria Разница
Показатель Scripting, мс 2955 2392 -563
Показатель Rendering, мс 3002 2525 -477
Показатель Painting, мс 329 313 -16
Общее время блокировки, мс 1862,66 994,07 -868


Сравнение процесса взаимодействия с разными вариантами страницы

Итоги


Вот и всё. Как видите, использование технологии CSS-in-JS, предусматривающей вычисление стилей во время работы страницы, оказывает заметное воздействие на производительность. Это актуально, в основном, для недорогих устройств и для регионов с медленными или дорогими интернет-каналами. Поэтому, возможно, нам следует ответственнее подходить к тому, какие именно инструменты мы выбираем, и к тому, как именно ими пользуемся. Отличный опыт разработчика не должен достигаться ценой ухудшения пользовательского опыта.

Я полагаю, что мы (разработчики) должны больше размышлять о том, каковы последствия выбора тех или иных инструментов. Когда я в следующий раз начну работу над новым проектом технологией CSS-in-JS я больше пользоваться не буду. Я либо применю обычный CSS, либо воспользуюсь альтернативой CSS-in-JS, библиотекой, занимающейся обработкой стилей во время сборки проекта и извлекающей стили из JS-бандлов.

Я думаю, что следующим значительным феноменом мира CSS станут CSS-in-JS-библиотеки, обрабатывающие стили во время сборки проектов. Дело в том, что появляется всё больше и больше таких библиотек (например свежайшая vanilla-extract от Seek). Да и крупные компании тоже двигаются в этом направлении, например Facebook.

Как вы относитесь к CSS-in-JS?


Подробнее..

Продвинутый CSS-in-TS

15.10.2020 20:21:55 | Автор: admin


Здравствуйте, меня зовут Дмитрий Карловский и я автор одного из первых фреймворков целиком и полностью написанных на тайпскрипте $mol. Он по максимуму использует возможности статической типизации. И сегодня речь пойдёт о максимально жёсткой статической фиксации стилей.


Это расшифровка выступления на PiterJS#46. Вы можете либо посмотреть видео запись, либо открыть в интерфейсе проведения презентаций, либо читать как статью...


Подопытное приложение


Разбирать мы будем весьма простое приложение $my_profile, которое состоит из двух панелей: меню и детализация. А каждая панель состоит из двух частей: заголовка и тела.



Генерация классов


Описывается такое приложение крайне простым кодом..


$my_profile $mol_view sub /    <= Menu $my_panel    <= Details $my_panel$my_panel $mol_view sub /    <= Head $mol_view    <= Body $mol_scroll

Не буду грузить вас этим птичьим языком. Скажу, лишь, что из него генерируется typescript класс следующего вида..


interface $my_profile extends $mol_view {    Menu(): $my_panel    Details(): $my_panel} )interface $my_panel extends $mol_view {    Head(): $mol_view    Body(): $mol_scroll} )

Каждый компонент это некоторый класс, который наследуется от другого компонента. И в нём, для каждого вложенного компонента генерится метод, который возвращает экземпляр соответствующего класа. Такие методы обычно пишутся с большой буквы.


Генерация DOM


Моловский компонент, когда рендерится, генерирует некоторый DOM-элемент. Имя этого элемента по умолчанию совпадает с именем инстанцированного класса компонента, что довольно удобно при обзоре сгенерированного DOM-дерева. При необходимости имя элемента может быть переопределено через свойство dom_name. Кроме того, автоматически генерируется набор атрибутов, предназначенных для подключения стилей. При этом учитывается не только собственное глобальное имя компонента, но и глобальные имена всех его предков, а так же его локальное имя во внешнем компоненте, глобальные имена внешнего компонента и его предков и так далее рекурсивно до корня приложения.


interface $my_profile extends $mol_view {    Menu(): $my_panel    Details(): $my_panel} )interface $my_panel extends $mol_view {    Head(): $mol_view    Body(): $mol_scroll} )

<mol_view    mol_view    my_panel_body    my_profile_details_body    >    <mol_button_major        mol_view        mol_button        mol_button_major        my_profile_signup        >

В данном примере мы видим следующие атрибуты:


mol_view базовый класс для всех моловских компонент, через него можно, например, сделать reset для вообще всех компонент, без риска поломать, не моловские компоненты.
my_panel_body значит это компонент с локальным именем Body внутри внешнего компонента $my_panel.
my_profile_details_body значит тот $my_panel имеет локальное имя Details в приложении $my_profile.


Наложение стилей


Таким образом мы получаем исчерпывающий набор атрибутов для стилизации компонент, без традиционного ручного прописывания классов элементам. И как бы глубоко ни располагался элемент специфичность селектора остаётся постоянной, что очень удобно, когда надо переопределять стили.


<mol_view    mol_view    my_panel_body    my_profile_details_body    >    <mol_button_major        mol_view        mol_button        mol_button_major        my_profile_signup        >

[my_profile_details_body] {    overflow: 'overlay';}[my_profile_details_body] [mol_button] {    border-radius: .5rem;}

Но у такого написания CSS есть ряд недостатков. Например, эти длинные селекторы утомительно писать руками. Среда разработки не понимает, что это имена компонент и ничего не подсказывает при наборе. Наконец, тайпскрипт тоже ничего про это соглашение не знает и не может протайпчекать селекторы.


Генерация стилей


Было бы классно описать наши стили прямо в тайпскрипте в простой лаконичной форме и автоматически сгенерировать из этого CSS.


[my_profile_details_body] {    overflow: 'overlay';}[my_profile_details_body] [mol_button] {    border-radius: .5rem;}

$mol_style_define( $my_profile , {    Details: {        Body: {            overflow: 'overlay',            $mol_button: {                border: {                    radius: rem(.5),                },            },        },    },} )

Тут мы вызываем функцию $mol_style_define, которая генерит StyleSheet. Передаём в неё класс компонента $my_profile и JSON, говорящий, что внутри компонента Details и его внутреннего компонента Body стили такие-то, а для всех вложенных в него кнопок сякие-то.


CSSOM: проблема с редактированием через DevTools


Генерировать стили можно двумя способами: либо через CSSOM, либо через генерацию портянки CSS и подклеивания его через элемент style. Если использовать первый подход, то в Chrome Dev Tools такие стили становятся не редактируемыми, что очень не удобно при разработке. Поэтому приходится использовать второй подход.



Сверху на скриншоте вы видите стили, сгенеренные библиотекой aphrodite, а снизу обычный CSS. Кстати, обратите внимание на порнографию в качестве селектора и сравните с теми именами, что генерит $mol_view.


Генерация CSS довольно простая операция. У меня это заняло 3КБ кода. Так что не будем особо на этом останавливаться и перейдём к типизации..


CSSStyleDeclaration: слабая типизация


В идеальном мире мы бы взяли стандартный тип CSSStyleDeclaration, поставляемый вместе с тайпскриптом. Это просто словарь из 500 свойств, типизированных как string.


type CSSStyleDeclaration = {    display: string    // 500 lines}

Соответственно, вы можете легко опечататься в каком-нибудь слове, а тайпскрипт это злополучно пережуёт и даже не подавится.


{    display: 'black' // }

csstype: кривая типизация


Можно взять популярную библиотеку csstype, которая генерируется из выгрузки всех свойств и их значений из MDN. Во второй её версии генерируются не очень полезные типы..


type DisplayProperty =| Globals| DisplayOutside| DisplayInside| DisplayInternal| DisplayLegacy| "contents" | "list-item" | "none"| string

Кто знаком с тайпскриптом, понимаю, что этот код эквивалентен следующему..


type DisplayProperty = string

Всё дело в том, что строковые литералы являются подтипами строки, потому тайпскрипт оставляет лишь наиболее общий тип.


csstype@3: кривая типизация с подсказками


Но ничего, в тетьей версии они это "починили" string соединили с пустым интерфейсом, из-за чего слияния с литералами уже не происходит:


type Display =| Globals| DisplayOutside| DisplayInside| DisplayInternal| DisplayLegacy| "contents" | "list-item" | "none"| (string & {})

Теперь типы не сливаются и подсказки всё же работают, но вы по прежнему можете опечататься и никто не ругнётся..


{    display: 'black' // }

Простые свойства


Мы пойдём своим путём и самостоятельно опишем типы для нужных нам свойств в том виде, который будет нам удобнее всего. Для этого заведём интерфейс Properties и будем заполнять его..


interface Properties {    /**     * Whether an element is treated as a block or inline element     * and the layout used for its children, such as flow layout, grid or flex.     */    display?: 'block' | 'inline' | 'none' | ... | Common    // etc}type Common = 'inherit' | 'initial' | 'unset'

Подсказки по свойствам


Для каждого свойства будем писать докстринг, который будет выводиться над именем свойства при наведении, что полезно тем, кто ещё не вызубрил все 500 CSS-свойств.



Группы свойств


Многие CSS свойства имеют общий префикс и было бы удобно не писать его каждый раз, а сгруппировать в один объект..


overflow: {    x: 'auto' ,    y: 'scroll',    anchor: 'none',}

interface Properties {    overflow? : {        x?:  Overflow | Common        y?:  Overflow | Common        anchor?: 'auto' | 'none' | Common    }}type Overflow = 'visible' | 'hidden' | ... | Common

Свойства размеров


Для размерностей есть не только предопределённый список значений, но произвольные юниты (1px, 2rem) и функции (calc(1rem + 1px)).


interface Properties {    width?: Size    height?: Size}type Size =| 'auto' | 'max-content' | 'min-content' | 'fit-content'| Length | Commontype Length = 0 | Unit< 'rem' | ... | '%' > | Func<'calc'>

Единицы измерения


Юнит можно объявить как просто класс, который параметризирован литералом и который умеет правильно себя сериализовывать.


class Unit< Lit extends string > {    constructor(        readonly val: number,        readonly lit: Lit,    ) { }    toString() {        return this.val + this.lit    }}

Так как литерал мы передаём в конструкторе, то типы экземпляров созданных с разными литералами будут различны, что не позволит, например, передать проценты туда, где ожидаются градусы.


function rem( val : number ) {    return new Unit( val , 'rem' )}{    width: rem(1) // Unit<'rem'>}

Функции


Аналогичным образом можно реализовать и функции. В них класс параметризируется не только именем функции, но и типом её содержимого.


Func<    Name extends string,    Value = unknown,> {    constructor(        readonly name: Name,        readonly val: Value,    ) { }    toString() {        return `${ this.name }(${ this.val })`    }}

function calc( val : string ) {    return new Func( 'calc' , val )}{    // Func< 'calc' , string >    width: calc( '1px + 1em' )}

Сокращённые свойства


Многие CSS свойства имеют как полную так и сокращённую формы. Например, для margin можно указать от 1 до 4 значений. И если для 1 и 2 всё более-менее понятно, то с болшим числом начинаются головоломки типа: если значения 4, то margin-left это последнее значение, а если 3, то предпоследнее. Чтобы такого не происходило, оставим лишь пару сокращённых форм, а если хочется большего контроля изволь написать в полной форме какому направлению какое значение. Получаем чуть больше писанины, но и улучшаем понятность кода.


interface Properties {    margin?: Directions<Length>    padding?: Directions<Length>}type Directions< Value > =| Value| [ Value , Value ]| {    top?: Value ,    right?: Value ,    bottom?: Value ,    left?: Value ,}

margin: rem(.5)padding: [ 0 , rem(.5) ]margin: {    top: 0,    right: rem(.5),    bottom: rem(.5),    left: rem(.5),}

Цвета


Для цветов у нас есть словарь $mol_colors из всех стандартных цветов просто берём из него ключи. Плюс добавляем несколько функций..


type Color =| keyof typeof $mol_colors| 'transparent' | 'currentcolor'| $mol_style_func< 'hsla' | 'rgba' | 'var' >color?: Color | Common

{    color: 'snow',    background: {        color: hsla( 0 , 0 , 50 , .1 ),    },}

hsl и rgb специально не добавлены, ибо написать лишнюю единичку для hsla и rgba не проблема, зато АПИ несколько упростили.


Списки


Порой свойство может содержать не одно значение, а целый список. Например, список фоновых картинок..


background?: {    image?: [ $mol_style_func<'url'> ][]}

background: {    image: [        [url('/foo.svg')],        [url('/bar.svg')],    ],},

Тут можно было бы запариться со специальными хелперными функциями, но писать их постоянно не очень практично, поэтому используется следующая простая эвристика: если в массиве лежат простые значения, то они просто соединяются через пробел. А если сложные (массивы, объекты), то через запятые. А вложенные в них значения уже через пробел.


Списки структур


Более сложный пример список из структур как в описании теней..


box?: {    shadow?: readonly {        inset: boolean        x: Length        y: Length        blur: Length        spread: Length        color: Color    }[]}

box: {    shadow: [        {            inset: true,            x: 0,            y: 0,            blur: rem(.5),            spread: 0,            color: 'black',        },    ],},

У тени много параметров, в которых легко запутаться, поэтому лучше писать имена параметров явно. При сериализации имена параметров отбрасываются, а значения соединяются через пробел. Особый случай булево свойство, если оно ложно, то игнорируется, а если истинно, то добавляется имя свойства.


БЭМ-элементы


Наконец, мы дошли до самой мякотки. У нас есть компонент $my_profile, в котором есть элемент Details, который сам является компонентом $my_panel, в котором есть элемент Body, который является компонентом $mol_scroll. И было бы неплохо уметь стилизовать любой из этих элементов, через стили, задаваемые компоненту $my_profile.


interface $my_profile {    Details(): $my_panel} )interface $my_panel {    Body(): $mol_scroll} )

$mol_style_define( $my_profile , {    padding: rem(1),    Details: {        margin: 'auto',        Body: {            overflow: 'scroll',        },    },} )

При этом хочется, чтобы тайпскрипт проверил, что указанные в стилях элементы действительно присутствуют в компонентах.


Поиск всех БЭМ-элементов


У нас есть специальная типофункция $mol_type_pick, которой вы передаёте некоторый интерфейс и желаемый тип полей, а возвращает она тот же интерфейс, но без полей, которые не соответвуют желаемому типу.


$mol_type_pick<    $my_profile,    ()=> $mol_view,>

interface $my_profile extends $mol_view {    title(): string    Menu(): $my_panel    Details(): $my_panel} )        {    Menu(): $my_panel    Details(): $my_panel}

В данном примере, у компонента есть свойство title, возвращающее строку. Но было бы странно вешать стили на строку. Стили можно вешать лишь на Menu и Details ибо они возвращают экземпляры компонент.


БЭМ-блоки


Вложенные компоненты порой хочется стилизовать не только по их локальному имени, но и по имени их класса. Например, мы хотим сделать вообще все кнопки по умолчанию красными, а кнопки на панели детализации синими.


interface $my_profile {    Menu(): $my_panel    Details(): $my_panel} )interface $mol_button {} )

$mol_style_define( $my_profile , {    $mol_button: {        color: 'red',    },    Details: {        $mol_button: {            color: 'blue',        },    },} )

Поиск всех подклассов


Найти все имена компонент не сложно, так как все классы, функции и прочие глобальные переменные объявляются в едином неймспейсе с именем $. Это позволяет воспользоваться тем же $mol_type_pick, чтобы выбрать все компоненты.


type $mol_view_all = $mol_type_pick<    typeof $,    $mol_view,>

namespace $ {    export class $mol_view {}    export class $my_panel {}    export class $mol_tree {}}        {    $mol_view: typeof $mol_view    $my_panel: typeof $my_panel}

Декларативные ограничения


Наконец, мы добрались до типизации функции-генератора в которую мы передаём компонент и описание стилей. Наивное описание типов для неё выглядит в декларативной форме по классу компонента генерируется развесистый рекурсивный тип со всеми возможными вариантами полей.


function $mol_style_define<    View extends typeof $mol_view,    Config extends Styles< View >>(    view : View,    config : Config)

type Config< View extends $mol_view > = {    $my_panel: {        $my_panel: {            ...        }        $my_deck: {            $my_panel: {                ...            }        }        ...    }    ...}

Проблема тут в том, что вкладывать друг в друга компоненты вы можете практически как угодно, и число различных комбинаций растёт экспоненциально от глубины. В результате тайпскрипт выжирает всю память и падает. А если и не падает, то начинает дико тормозить.


Не понятные типошибки


Но даже если вы дождётесь вывода типа, то сообщения ошибки вас не обрадуют, ибо из него не будет понятно где и почему возникла ошибка.



Императивные ограничения


Лучше в таком случае использовать императивный подход, где вы берёте фактический тип написанного пользователем JSON, в типогуарде проходитесь по нему и переданному компоненту и генерируете новый не рекурсивный тип, где оставлены лишь валидные поля и значения. А на не валидных тайпскрипт не сматчится и сообщит об ошибке.


function $mol_style_define<    View extends typeof $mol_view,    Config extends StylesGuard<        View,        Config,    >>(    view: View,    config: Config)

{    Details: {        foo: 'bar',    },}        {    Details: {        foo: never,    },}

Работает такой подход гораздо быстрее и кушает меньше памяти. Важно б этом помнить, когда пишете сложные типы, иначе придётся ждать подсказок по несколько секунд и порой получать фигу.


Всё ещё непонятные ошибки


Если просто возвращать never для не валидных значений, то сообщения об ошибке хоть и будут иметь координаты, но не будет понятна причина, почему написанный код не валиден.



Понятные ошибки


Вместо never лучше возвращать специальный тип, который ни с чем не сматчится и в котором литералом прописать суть сообщения об ошибке и дополнительную информацию.



В примерах мы сначала попытались навесить стили на несуществующий атрибут, потом на свойство, которое возвращает не компонент, а в конце просто написали какую-то балалайку, которую не удалось распознать ни как имя компонента, ни как имя элемента, ни как имя свойства.


Атрибуты


Кстати, об атрибутах. В $mol_view они задаются как метод возвращающий словарь из строки в примитив.


attr *    ^    mol_link_current <= current false        attr() {    return {        ... super.attr(),        mol_link_current: this.current(),    }}

Стили для атрибутов


Для описания стилей для атрибутов просто вкладываем друг в друга: собачка -> имя атрибута -> значение -> (стили, элементы и прочий фарш).


{    '@': {        mol_link_current: {            true: {                zIndex: 1            }        }    }}

Псевдоклассы и псевдоэлементы


Разумеется есть предопределённый список всех псевдоклассов и псевдоэлементов, которые можно использовать на любом уровне..


{    ':hover': {        zIndex: 1    }}

Медиа запросы


Так же можно вкладывать медиа-запросы. Это гораздо удобнее, чем в CSS, где медиа-запросы пишутся вне и требуют повторения селектора внутри.


{    $mol_scroll: {        overflow: 'scroll',        '@media': {            'print': {                overflow: 'visible',            },        },    },}

Непосредственно вложенные блоки


Наконец, с помощью угловой скобочки можно задавать стили лишь для непосредственно вложенных элементов.


{    '>': {        $mol_view: {            margin: rem(1),        },        $mol_button: {            margin: rem(0),        },    },}

Что получилось


  • Каскадные переопределения стилей
  • Тайпчек всех ключей и значений
  • Подсказки по всем ключам и значениям
  • Описание всех свойств
  • Понятные сообщения об ошибках
  • Удобство описания стилей

Что можно улучшить


  • Рантайм чтение стилей до рендеринга (полезно для виртуализации)
  • Типизация всех свойств (прогресс 10 из 500)
  • Добавить все функции
  • Поддержать анимации
  • Типизированные выражения в calc (а не строка)

Попробовать вне $mol


Ести вас заинтересовал мой расказ и вы хотели бы попробовать поиграться с этим, но не готовы ещё перейти на $mol, то можете воспользоваться библиотекой mol_style_all, позволяющей описывать CSS-свойства.


import {    $mol_style_unit,    $mol_style_func,    $mol_style_properties,} from 'mol_style_all'const { em , rem } = $mol_style_unitconst { calc } = $mol_style_funcconst props : $mol_style_properties = {    margin: [ em(1) , rem(1) ],    height: calc('100% - 1rem'),}

codesandbox.io/s/molstyleall-ked9t


Там, конечно, нет генератора CSS, так как он сильно завязан на то, что генерирует $mol_view. Тем не менее, если вы заходите добавить эту функциональность к своему любимому фреймворку, то заходите к нам в чат mam_mol вместе подумаем, как бы это сделать.


Продолжение следует...


К сожалению, материала оказалось слишком много, поэтому пришлось разбить выступление на две части. Это была лайтовая чать про использование, а следующая уже будет хардкорная, где мы поговорим про программирование на типах и как реализовывать всё то, о чём я рассказал..


  • Сравнение типов
  • Типофункции
  • Типотесты
  • Типошибки
  • Типогуарды
  • Фильтрация интерфейсов
  • Брендированные примитивы

Куда пойти



Обратная связь


  • Хороший доклад. Хотелось бы побольше конкретных примеров. Вот у нас есть отрисованный дизайн, и вот, что мы написали, чтобы получился pixel-perfect.
  • Очень быстро говорит! Не успеваешь понять и думаешь, что надо пересматривать и гуглить.
  • Не работал с ТС, поэтому не могу обьективно оценить ценность трудов Дмитрия.
  • Для меня не сильно профильно, непонятно и не очень интересно.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru