Продвинутый 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 | Common
    
    type 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_unit
    const { calc } = $mol_style_func
    
    const 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.
    • Очень быстро говорит! Не успеваешь понять и думаешь, что надо пересматривать и гуглить.
    • Не работал с ТС, поэтому не могу обьективно оценить ценность трудов Дмитрия.
    • Для меня не сильно профильно, непонятно и не очень интересно.

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

    Что думаете?

    • 5,5%Хочу $mol здесь и сейчас4
    • 2,7%Хочу $mol_style в свой ${фреймворк}2
    • 1,4%Хочу ${формат-стилей} в этот ваш $mol1
    • 91,8%Ерунда какая-то, ничего не хочу67

    Комментарии 49

      –2
      Кто проголосовал за «Хочу $mol здесь и сейчас»? :D Признавайтесь :D
        +2
        Спасибо за статью, вы не думали, что если бы ваш фреймворк использовал общепринятый code style — у него было бы намного больше последователей?
          –8

          Программисты, выбирающие технологию по код стайлу, не являются целевой аудиторией.

            +1

            Вам решать, но хороший код стайл не менее важен чем хорошая архитектура

              0

              А с чего вы взяли, что это у нас плохой, а привычный вам — хороший?

                +1
                а чем у вас хороший? Зачем мне эти $/\_? $.$$ когда можно обойтись без них?
                это удобно писать? Вместо того, что-бы писать переменную только буквами, вы к ней еще предлагаете $_ или $mol_
                что читабельней
                $mol_table
                или
                table?

                а если название будет длиннее?
                $mol_users_table_for_students_page
                или
                UsersTableForStudentsPage
                  –2
                  Зачем мне эти $/_? $.$$ когда можно обойтись без них?

                  Можно и без них, только придётся писать в 2 раза больше кода. Тут можете глянуть на примере Реакта: https://github.com/nin-jin/HabHub/issues/6


                  что читабельней $mol_table или table?

                  Какой именно table вы имеете ввиду? Я нашёл 6:


                  $mol_grid_table
                  $mol_app_report_table
                  $mol_list_demo_table
                  $mol_perf_uibench_table
                  $mol_dev_format_table
                  $mol_log2_table


                  $mol_users_table_for_students_page или UsersTableForStudentsPage

                  Любители верблюдов обычно убеждены сами и пытаются убедить всех окружающих, что второе читать легче. Однако, это не так. Первая форма записи лишена неоднозначностей с аббревиатурами и слова чётко различимы, а не сливаются в единое полотно. Обоснование можете почитать тут: https://github.com/eigenmethod/mol/wiki/PascalCase-camelCase-kebab-case-snake_case

                    +2
                    Любители верблюдов обычно убеждены сами и пытаются убедить всех окружающих, что второе читать легче. Однако, это не так. Первая форма записи лишена неоднозначностей с аббревиатурами и слова чётко различимы, а не сливаются в единое полотно. Обоснование можете почитать тут: github.com/eigenmethod/mol/wiki/PascalCase-camelCase-kebab-case-snake_case


                    По поводу пункта «Проблемы с аббревиатурами: mxBADownload» соглашусь, по поводу остального — нет, когда много кода — camelCase элементарно компактнее и опрятнее смотрится, из-за чего легче воспринимается, а с монохромным шрифтом отлично читается

                    Какой именно table вы имеете ввиду? Я нашёл 6:

                    Тот, который импортнули в начале файла
                      +1

                      Если вы не написали IDE, которая на ходу переводит все идентификаторы и стандартной и сторонних билиотек JS/TS в snake, вероятно, нам надо читать раздел смешение стилей?

                        0

                        Нет. Более того, разное именование позволяет понять какой метод из стандартной библиотеки, а какой — нет.

                          +1

                          Наверное, наоборот, какой из mol а какой нет. Какой из нестандартной но не вашей библиотеки, наверное, не позволяет.

                +1
                Код пишется программистами для программистов. Сложновоспринимаемый код стайл вполне может стать весомым аргументом против (для проектов сложнее туду-аппы из примеров в документации, размером в «100 строк»)
                  –1

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

              –1
              Интересный велосипед.

              У меня на проекте сейчас реактом и styled-components. То есть, весь CSS описывается на уровне компонента, стили проверяются vscode-styled-components. Может быть и не идеально, но каких-то особенных проблем не доставляет.
                +1

                А вообще, вы можете сравнить концептуально $mol c каким-то близким фреймворком — он действительно несет что-то новое? Вот автор постоянно постит статьи типа "всего за полчаса я сделал аналог экселя" — там действительно есть какие-то высокоуровневые абстракции которых нет у других или это что-то еще?


                И еще. Во фронтенде это принято постоянные префиксы вот такие? Вместо "мама мыла раму" "$mol_мама $mol_рыла $mol_маму"

                  0
                  У автора есть демы этого всего, и по демам в принципе видно, что «всего за полчаса» таки ничего не делается — почти всеми неискусственными демами пользоваться нельзя (включая тот самый showcase.hyoo.ru, где загрузка каждого элемента делает к нему скролл, из-за чего до полной загрузки всего страница припадочно дёргается). То есть, функционал есть, удобства пользования или даже просмотра — примерно никакого.
                    –1

                    Нашли пару багов — это ещё не "пользоваться нельзя". Не преувеличивайте.


                    Если вам, вдруг, интересно, почему так происходит. showcase.hyoo.ru открывает кучу приложений на одном экране. Каждое приложение контролирует свой скролл. И если оно где-либо вызывает scrollIntoView, то скролл происходит не только в нём, но и во внешнем приложении. Вот такие у нас кривые веб-технологии. Скорее всего придётся переделать витрину так, чтобы единомоментно было видно лишь одно приложение.


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

                    +1
                    И еще. Во фронтенде это принято постоянные префиксы вот такие? Вместо «мама мыла раму» "$mol_мама $mol_рыла $mol_маму"


                    Это фишка автора, как раз из-за таких вот фишек фреймворком мало кто пользуется.
                      0
                      можете сравнить концептуально $mol c каким-то близким фреймворком — он действительно несет что-то новое?

                      Тут я рассказывал про отличительные особенности:
                      https://github.com/nin-jin/slides/tree/master/mol
                      https://github.com/nin-jin/HabHub/issues/23


                      там действительно есть какие-то высокоуровневые абстракции которых нет у других или это что-то еще?

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


                      Во фронтенде это принято постоянные префиксы вот такие? Вместо "мама мыла раму" "$mol_мама $mol_рыла $mol_маму"

                      Не принято. Обоснование тут: https://github.com/eigenmethod/mol/wiki/Fully-Qualified-Names-vs-Imports

                        0
                        Не принято. Обоснование тут: github.com/eigenmethod/mol/wiki/Fully-Qualified-Names-vs-Imports

                        все эти плюсы переплюнет автоимпорт от любой нормальной ide
                        в итоге минусы остаются, а именно: длинные имена которые нужно писать каждый раз, вместо импорта вверху файла который делается автоматом и скрыт от глаз по умолчанию
                        Если есть конфликт имен — в импорте можно сделать {user as molUser}, но такое бывает очень редко
                          0
                          все эти плюсы переплюнет автоимпорт от любой нормальной ide

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


                          длинные имена которые нужно писать каждый раз

                          Вам ничто не мешает сделать локальный алиас в любом месте, где вам будет удобно:


                          const table = $mol_log2_table

                          такое бывает очень редко

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

                            +2
                            Автоимпорт не отработает для ещё не скачанного кода. MAM же автоматически выкачает нужный репозиторий.

                            Зачем? Я не добавляю по пакету в минуту, а раз в неделю написать в консоли npm i --save ngxs — у меня не составляет труда.

                            А как в этих автоимпортах приятно разрешать конфликты — неописуемо просто.

                            Можно пример?

                            Вам ничто не мешает сделать локальный алиас в любом месте, где вам будет удобно:

                            Чем тогда это лучше импорта? Если не брать автоматическое скачивание пакета

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


                            В мозгу есть такая штука, как контекст, если вы работаете с чем-то достаточно давно — тогда мозг сам понимает, что это тут за table.
                            Если вы впервые в проекте — тогда вам все равно нужно будет разбираться что это за $mol_table
                              0
                              раз в неделю написать в консоли npm i --save ngxs — у меня не составляет труда.

                              А как часто вы проверяете какие модули пора бы удалить?


                              Чем тогда это лучше импорта?

                              Тем, что место любое, а не только начало файла. Вплоть до отсутствия локального алиаса — актуально, когда использование в файле единично.


                              В мозгу есть такая штука, как контекст

                              А у контекста есть такая характеристика, как размер. У человеков он не очень большой: https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B1%D0%BE%D1%87%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C


                              Если вы впервые в проекте — тогда вам все равно нужно будет разбираться что это за $mol_table

                              Один раз разберётся и будет чёткая ассоциативная связь имени со смыслом. С короткими именами нужно постоянно держать в рабочей памяти ещё и постоянно меняющуюся таблицу мапинга имён на семантику.

                                0
                                Один раз разберётся и будет чёткая ассоциативная связь имени со смыслом. С короткими именами нужно постоянно держать в рабочей памяти ещё и постоянно меняющуюся таблицу ремапинга имён.

                                Вместо этого нужно держать путь к файлу, что ничем не лучше
                                ведь $mol_view — это mol/view, если этот путь будет сложнее — тогда это то же самое что запоминать алиас, а если путь изменится — тогда кроме рефакторинга придется запоминать новое название, не нужно выставлять свое решение как серебряную пулю, у него не меньше минусов чем в критикуемом вами

                                А как часто вы проверяете какие модули пора бы удалить

                                Убираю модуль из кода — нажимаю command + b что-бы посмотреть используется где-то модуль в проекте или нет, если нет — удаляю из package.json,
                                  0
                                  не нужно выставлять свое решение как серебряную пулю

                                  Не помню, чтобы я таким занимался. Я объяснил, почему было выбрано именно это решение.


                                  Убираю модуль из кода — нажимаю command + b

                                  И никогда не забываете?

                                    0
                                    Не помню, чтобы я таким занимался. Я объяснил, почему было выбрано именно это решение.

                                    Так почему именно ваше решение? Если оно в конечном итоге не лучше чем общепринятое?
                                    И никогда не забываете?

                                    Нет, а как можно забыть? Может у меня не такие огромные проекты, что-бы было миллион установленных сторонних пакетов.
                                    Например в проекте на angular у меня обычно:
                                    @ngxs, @ngxs-labs/data, angular/cdk, until-destroy, lodash-es, очень редко устанавливаем что-то еще
                                      –1
                                      Если оно в конечном итоге не лучше чем общепринятое?

                                      Лучше, конечно. Какой вообще может быть смысл выбирать не лучшее решение?

                            0

                            На тему автоимпорта есть такое исследование:
                            https://gist.github.com/zerkalica/d69c267d4bbac9cd49047e22336d5110


                            Впору в компанию к инженеру по настройке WebPack уже начинать нанимать инженера по настройке автоимпортов.

                              –1
                              Впору в компанию к инженеру по настройке WebPack уже начинать нанимать инженера по настройке автоимпортов.

                              А есть такие? Я бы нанял

                              А если серьезно, то обычно в шторме все работает из коробки, а настройку вебпака в моем случае на себя берет angular-cli, так же unused imports удаляются при коммите
                      0

                      Чувство прекрасного снова подвело, учитывая картинку к посту. По фреймворку: Дмитрий просто занимается творчеством.

                        0
                        Обьясните ньюфагу зачем нам css в TS. Стили удобно размещать отдельно, sass позволяет делать всё что нужно. Текущие известные технологии позволяют закрывать любые задачи бизнеса. В статье я не увидел какой-то новой проблемы, которую решает mol. Синтаксис немного своеобразный, знаете ли. Создать свой инструмент, потому что можете — это конечно классно, не отрицаю. Но мне не понятна конечная цель этого проекта, не ясно какие головные боли разработчиков решает mol.

                        Конечно, можно из буханки хлеба сделать троллейбус — но зачем?
                          +2
                          Конкретно авторское решение (как обычно несовместимое ни с чем) — конечно едва ли.
                          В целом по CSS-in-code решениям — я за весь свой програмистский опыт так и не увидел другого способа держать CSS в чистоте без выделения на это отдельного специального человека. То есть да, в идеале бы пользоваться нативными таблицами стилей и ничего не выдумывать, но на практике на любых не write once проектах это превращает CSS в гадюшник из неиспользуемых и плохо организованных стилей, в который надо отдельно ходить делать там уборку. Регулярно — поэтому и в некотором пределе проблема превращается в «нам нужен отдельный человек на должность клининг-менеджера CSS».

                          С занесением CSS в код — его становится возможным обрабатывать, как код. То есть, например, проверять ссылки на используемость линтером — отлетает проблема неиспользуемых стилей; или проверять синтаксическую валидность — отлетает проблема «опечатался в одной букве и что-то где-то может непоправимо полететь», например, кнопка важная пропадёт. Плюс, организация CSS начинает естественным образом цепляться к организации кода, а не быть совершенно отдельной сущностью. Плюсов много, и они очень весомые. Плата за них — отсутствие стандартизации этого всего зоопарка (впрочем, навести мосты к системам, могущим принимать/выдавать обычный css, как тот же emotion — несложно), плюс пострадавшее быстродействие. Например, styled-components в прошлом (не знаю, как сейчас) сами по себе давали ну очень даже неиллюзорные тормоза.
                            0
                            отлетает проблема «опечатался в одной букве и что-то где-то может непоправимо полететь», например, кнопка важная пропадёт


                            Извольте! Отсутствие опечаток не гарантирует того, что все отобразится как на макете дизайнера. И Вам в любом случае это надо проверять (пример — тесты скриншотами). А если это нужно проверять, то каки-либо преимуществ от того, что в стилях у вас TS (и это уже вовсе не стили, а js-код) — нету, по сравнению, скажем, со здоровым CSS-In-JS:

                            const Button = styled.a`
                              background: transparent;
                              color: white;
                              border: 2px solid white;
                            `


                            Линтер не пропустит опечатки в прод, удобство работы с css сохранено (форматирование, копи-паста из браузера и прочие ништяки), тесты писать так и так нужно.
                              0
                              опечаток не гарантирует того, что все отобразится как на макете дизайнера

                              Зато гарантирует что не отобразится.


                              А если это нужно проверять, то каки-либо преимуществ от того, что в стилях у вас TS (и это уже вовсе не стили, а js-код) — нету

                              Слишком категорично. Автоматическая проверка всегда большое подспорье. Вопрос лишь приносит ли она больше пользы, чем вреда. Что в случае этого топика не очевидно (слишком много возни и итоговый json-подобный вид имхо это боль).


                              Следуя вашему аргументу легко придти к:


                              • тесты не нужны, всё равно руками проверять
                              • типы не нужны, всё равно руками проверять
                              • линтеры не нужны, ...
                                0
                                тесты не нужны, всё равно руками проверять


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

                                Следуя вашему аргументу легко придти к:

                                типы не нужны, всё равно руками проверять
                                линтеры не нужны, ...


                                Вы слишком смело расширили контекст css на область разработки целиком. Мои высказывания касались лишь css в виде JS объектов с TS против css в виде css с линтерами. Где я как раз аргументировал за линтеры и тесты.

                            0

                            Статья не о моле, а о его системе статической стилизации. Если интересно почитать конкретно про мол, и как он решает все остальные проблемы человечества, то я дал ссылки на соответствующие статьи выше: https://habr.com/ru/post/523646/#comment_22189110

                              +3
                              Статья не о моле

                              Мне кажется вы разучились писать статьи не о $mol. Я думаю даже если вы напишете статью "Как построить скворечник" там будет опросник про $mol и пару абзацев про кривые фронтенд фреймворки :)


                              Вроде толковая статья, как минимум, с точки зрения — как заставить TS плясать по своим правилам. Я почерпнул для себя пару интересных моментов и даже поставил "+". Но думаю ваши пассажи про mol всё равно заруинят и рейтинг статьи, и вообще какое бы то ни было позитивное отношение к контенту.


                              Я полагаю у хабра-сообщества уже давно выработался рефлекс: "так что это у нас, о, интересно, ах блин опять этот $mol". И дальше критическое мышление слегка подотключается.

                            –1
                            CSS вообще самая меньшая из всех проблем.
                            SASS + css modules идеально справляются с любым кейсом.
                            Даже если в процессе появятся какие-то неиспользуемые стили, вообще пофиг, ни на что не влияет.

                            Как обычно развели проблему на ровном месте, что проблемой не является вовсе. А вот говнокод который пишут тоннами, вот это настоящая проблема.
                              0
                              SASS + css modules идеально справляются с любым кейсом

                              Мой опыт показывает что всегда когда хочется сделать что-то "против шерсти" с css-modules возникают проблемы.


                              К примеру для css-modules есть линтер, который даже умеет проверять есть ли в файле не используемые стили. Но, он работает только для "1 TS\JS файл = 1 CSS файл" подхода. Что лично мне кажется часто неудобным. И тут либо пишешь как все и не выпендриваешься. Либо уже далеко не любой кейс решается.


                              Или скажем нужно импортировать какое-то имя класса из другого модуля. А нельзя. Точка. Они полностью независимы. Да в этом есть резон. Но когда нужно скосить углы, в виду какой-нибудь специфики… нельзя скосить углы.


                              Или скажем хочется сделать набор переиспользуемых анимаций? А как? Похоже только через `:global()`` либо дубляж.


                              В css-modules многое можно было бы сделать… более гибким. Но ввиду их философии — никто не будет. Это можно по разному трактовать. И как "а нефиг писать говнокод" и как "смерительная рубажка от людей которые думаю что они умнее всех". Но одно точно — "идеально справляются с любым кейсом" это ложь.


                              Добавить к этому что и у SCSS\SASS есть свои проблемы. И получаем что нет никаких идеальных инструментов. Есть только набор компромиссов.

                                0
                                Похоже только через `:global()`` либо дубляж.

                                Анимация либо глобальная, либо дубляж. Логично


                                Или скажем нужно импортировать какое-то имя класса из другого модуля. А нельзя. Точка. Они полностью независимы.

                                Можно подробнее кейс? import styles from "shared.module.css" валидная конструкция, так можно делать хоть в нескольких файлах. А что тогда подразумевается под "А нельзя. Точка."?

                                  0
                                  Можно подробнее кейс? import styles from "shared.module.css" валидная конструкция,

                                  Нужно только имена классов получить, для каскада. Не стили.


                                  Анимация либо глобальная, либо дубляж. Логично

                                  Для сравнения в мире JS такой проблемы не стоит. Есть импорты и экспорты. Не приходится ничего хардкодить.

                                    0
                                    import styles from "shared.module.css" валидная конструкция, так можно делать хоть в нескольких файлах

                                    Хм. Я не обратил внимание на синтаксис. Вы тут про JS. А я про нечто подобное:


                                    @import styles from './somepath.mod.scss';
                                    
                                    .myClass ${style.otherModuleClass} {
                                    }

                                    Вот про такое. Без CSS modules (голый глобальный CSS без ограничений) такое делается без каких-либо проблем. Многие другие решения из области CSS-in-JS на уровне уже webpack-импортов такое делают без проблем. В CSS-modules ЕМНИП так нельзя по причине идеологии чистого css кода.


                                    Но я мог что-то напутать или времена могли измениться.

                                      0

                                      Хм, да, в этом случае нельзя по той самой идеалогии. Может и к лучшему, что нельзя

                                0

                                Фантастика конечно. Как писать код так, чтобы его точно никто не понял. Мелькнули и сгорели фантазийные Sass (в синтаксисе на отступах) и CoffeeScript — их заменили Sass (SCSS) или CSS и JS/TS в совместимом и понятном виде. Но нет — найдутся желающие так же сверкнуть и невостребовано сгореть.

                                  +1
                                  CoffeeScript

                                  Я думаю он сгинул всё же не из-за табов :) Он просто стал никому не нужен. Выполнил свою роль "раскачать сообщество" и исчез. Будь проблема в табах, не был бы столь популярен python.

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

                                  По спецификации свойство display может состоять из двух слов. Например


                                  display: inline flex;

                                  Описывать такое в TS было бы достаточно многословным (перечислять все комбинации). Кстати, возможно, в будущем с этим сможет помочь template literal types из TS 4.1.


                                  Я так понимаю, что | string в csstypes для свойства display добавлен как раз для того, чтобы полный двухсловный синтаксис не ломал тайпскрипт.


                                  Как с этим обстоит у $mol?

                                    0

                                    Конкретно для display двойная форма ещё не поддерживается браузерами, так что типизирована лишь одинарная. Но по другим свойствам — используется подход со списками:


                                    type ContainRule = 'size' | 'layout' | 'style' | 'paint'
                                    
                                    /** Indicate that an element and its contents are, as much as possible, independent of the rest of the document tree. This allows the browser to recalculate layout, style, paint, size, or any combination of them for a limited area of the DOM and not the entire page, leading to obvious performance benefits. */
                                    contain?:
                                        | 'none' | 'strict' | 'content'
                                        | ContainRule | ContainRule[]
                                        | Common
                                      0

                                      С практической точки зрения двойная форма display пока не нужна, согласен. Но формально вы затипизировали подмножество css. Как вспомогательный инструмент для конкретного фреймворка — вполне пойдет.


                                      Contain у вас выглядит лучше, чем в csstypes. Интересно, почему в csstypes не сделали так же через массив. Возможно, из-за особенностей выгрузки формата из MDN (там же типы не вручную прописываются, а генерятся прямо из спецификации).


                                      На самом деле, насколько я вижу, основное отличие даже не в этих отдельных случаях, а в в shorthand-свойствах типа background. У вас по сути свой dsl для их описания (в виде вложенного объекта), из-за чего это еще плюс один синтаксис, который надо знать. Зато типизировать его гораздо проще, чем оригинальный формат.

                                        0

                                        Тайпскрипт подсказывает, так что знать особо не надо. Достаточно понимать принципы.

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

                                  Самое читаемое