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

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


Здравствуйте, меня зовут Дмитрий Карловский и я автор одного из первых фреймворков целиком и полностью написанных на тайпскрипте $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.
  • Очень быстро говорит! Не успеваешь понять и думаешь, что надо пересматривать и гуглить.
  • Не работал с ТС, поэтому не могу обьективно оценить ценность трудов Дмитрия.
  • Для меня не сильно профильно, непонятно и не очень интересно.
Источник: habr.com
К списку статей
Опубликовано: 15.10.2020 20:21:55
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Css

Typescript

$mol

$mol_view

$mol_style

$mol_type

Css-in-ts

Css-in-js

Категории

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

  • Имя: Макс
    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