Статьи

AbsurdJS или почему я написал свой собственный CSS-препроцессор

Как фронт-разработчик, я пишу много CSS, и использование чистого CSS в наши дни — не самый эффективный способ. Препроцессоры CSS — это то, что мне очень помогло. Мое первое впечатление было то, что я наконец нашел идеальный инструмент. У них есть множество функций, отличная поддержка, бесплатные ресурсы и так далее. Это все верно и до сих пор применяется, но после нескольких проектов я понял, что мир не так совершенен. Существует два основных CSS-препроцессора — LESS и SASS . Есть и другие, но у меня есть опыт работы только с этими двумя. В первой части этой статьи я поделюсь с вами тем, что мне не нравится в препроцессорах, а затем во второй части я покажу вам, как мне удалось решить большинство проблем, с которыми я столкнулся.


Независимо от того, какой препроцессор CSS задействован, всегда требуется настройка, например, вы не можете просто начать печатать файлы .sass или .sass и ожидать получения файла .css . Меньше требуется NodeJS и SASS Ruby. В данный момент я работаю в основном над приложениями HTML / CSS / JavaScript / NodeJS. Итак, LESS кажется лучшим вариантом, потому что мне не нужно устанавливать дополнительное программное обеспечение. Знаете, добавление еще одной вещи в вашу экосистему означает больше времени на обслуживание. Кроме того, вам нужен не только необходимый инструмент, но и все ваши коллеги теперь должны интегрировать новый инструмент.

Во-первых, я выбрал LESS, потому что у меня уже был установлен NodeJS. Он хорошо играл с Грантом, и я успешно завершил два проекта с этой настройкой. После этого я начал читать о SASS. Я интересовался OOCSS , Атомным дизайном и хотел построить прочную архитектуру CSS. Очень скоро я перешел на SASS, потому что это дало мне лучшие возможности. Конечно, мне (и моим коллегам тоже) пришлось установить Ruby.

Многие разработчики не проверяют полученный CSS. Я имею в виду, что у вас могут быть действительно хорошо выглядящие файлы SASS, но в итоге мы используем скомпилированный файл .css . Если он не оптимизирован и его размер файла велик, значит, у вас проблема. Есть несколько вещей, которые мне не нравятся в обоих препроцессорах.

Допустим, у нас есть следующий код:

1
2
3
4
5
6
7
// LESS or SASS
p {
    font-size: 20px;
}
p {
    padding: 20px;
}

Не думаете ли вы, что это должно быть скомпилировано в:

1
2
3
4
p {
    font-size: 20px;
    padding: 20px;
}

Ни LESS, ни SASS так не работают. Они просто оставляют ваши стили по мере их ввода. Это может привести к дублированию кода. Что делать, если у меня сложная архитектура с несколькими слоями, и каждый слой добавляет что-то к абзацу. Будет несколько определений, которые точно не нужны. Вы можете даже иметь следующую ситуацию:

1
2
3
4
5
6
p {
    font-size: 20px;
}
p {
    font-size: 30px;
}

Правильный код в конце должен быть только следующим:

1
2
3
p {
    font-size: 30px;
}

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

Объединение селекторов, которые используют одни и те же стили, — это хорошо. Насколько я знаю, LESS не делает этого. Допустим, у нас есть миксин, и мы хотим применить его к двум классам.

01
02
03
04
05
06
07
08
09
10
.reset() {
    padding: 0;
    margin: 0;
}
.header {
    .reset();
}
.footer {
    .reset();
}

И результат:

1
2
3
4
5
6
7
8
.header {
    padding: 0;
    margin: 0;
}
.footer {
    padding: 0;
    margin: 0;
}

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

1
2
3
4
.header, .footer {
    padding: 0;
    margin: 0;
}

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

01
02
03
04
05
06
07
08
09
10
%reset {
    padding: 0;
    margin: 0;
}
.header {
    @extend %reset;
}
.footer {
    @extend %reset;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
%reset {
    padding: 0;
    margin: 0;
}
%bordered {
    border: solid 1px #000;
}
%box {
    display: block;
    padding: 10px;
}
.header {
    @extend %reset;
    @extend %bordered;
    @extend %box;
}

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

01
02
03
04
05
06
07
08
09
10
11
.header {
    padding: 0;
    margin: 0;
}
.header {
    border: solid 1px #000;
}
.header {
    display: block;
    padding: 10px;
}

Это выглядит неправильно, не так ли? Должно быть только одно определение стиля и только одно свойство padding .

1
2
3
4
5
6
.header {
    padding: 10px;
    margin: 0;
    border: solid 1px #000;
    display: block;
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
.theme-dark() {
   color: #FFF;
   background: #000;
}
.theme-light() {
   color: #000;
   background: #FFF;
}
.component(@theme, @border) {
   border: «@{border} 1px #F00»;
   .theme-@{theme}();
}
.header {
   .component(«dark», «dotted»);
}

Конечно, у меня будет много тем, и они тоже должны быть миксинами. Таким образом, интерполяция переменных работает для свойства border, но не для имен mixin. Это просто, но в настоящее время это невозможно, или, по крайней мере, я не знаю, исправлено ли это. Если вы попытаетесь скомпилировать приведенный выше код, вы получите Syntax Error on line 11 .

SASS — это еще один шаг вперед. Интерполяция работает с заполнителями, что делает вещи немного лучше. Та же идея выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@mixin theme-dark() {
   color: #FFF;
   background: #000;
}
@mixin theme-light() {
   color: #000;
   background: #FFF;
}
%border-dotted {
   border: dotted 1px #000;
}
@mixin component($theme, $border) {
   @extend %border-#{$border};
   @include theme-#{$theme};
}
.header {
   @include component(«dark», «dotted»);
}

Итак, стилизация границ работает, но тема выдает:

1
Sass Error: Invalid CSS after » @include theme-«: expected «}», was «#{$theme};»

Это потому, что интерполяция в именах mixins и extends не допускается. Об этом идет долгая дискуссия , и, вероятно, она скоро будет исправлена.

И LESS, и SASS хороши, если вы хотите улучшить скорость написания, но они далеко не идеальны для создания модульного и гибкого CSS. В основном им не хватает таких вещей, как инкапсуляция, полиморфизм и абстракция. Или, по крайней мере, они не в той форме, которая мне была нужна.


Я боролся несколько дней с этим ограничением. Я потратил много времени на чтение документации. В конце концов я просто сдался и начал искать другие варианты. То, что у меня было недостаточно гибким, и я начал думать о написании своего собственного препроцессора. Конечно, это действительно сложная задача, и есть над чем подумать, например:

  • ввод — обычно препроцессоры берут код, который выглядит как CSS. Я предполагаю, что идея состоит в том, чтобы дополнить язык, например, добавить недостающие, но необходимые функции. Также легко портировать чистый CSS, и разработчики могут сразу начать использовать его, потому что на практике это почти тот же язык. Однако, с моей точки зрения, такой подход приносит мало трудностей, потому что мне пришлось все анализировать и анализировать.
  • синтаксис — даже если я напишу часть синтаксического анализа, мне пришлось изобрести свой собственный синтаксис, который является своего рода сложной работой.
  • конкуренты — уже есть два действительно популярных препроцессора. У них хорошая поддержка и активное сообщество. Вы знаете, большинство самых крутых вещей в нашей сфере очень полезны благодаря вкладчикам. Если я пишу свой собственный CSS-препроцессор и не получаю достаточного количества отзывов и поддержки от людей, я могу быть единственным, кто фактически использует его.

Итак, я немного подумал и нашел решение. Нет необходимости изобретать новый язык с новым синтаксисом. Это уже там. Я мог бы использовать чистый JavaScript. Уже существует большое сообщество, и многие люди могут сразу начать использовать мою библиотеку. Вместо чтения внешних файлов, их анализа и компиляции я решил использовать экосистему NodeJS. И, конечно же, самое главное — я полностью удалил часть CSS. Написание всего на JavaScript сделало мое веб-приложение намного чище, потому что мне не приходилось иметь дело с форматом ввода и всеми теми процессами, которые производят настоящий CSS.

(Название библиотеки — AbsurdJS . Вы можете посчитать это название смешным, и это действительно так. Когда я делюсь своей идеей с несколькими друзьями, они все сказали, что написание вашего CSS на JavaScript — абсурд . Так что это было идеальное название.)


Для использования AbsurdJS вам необходимо установить NodeJS. Если у вас все еще нет этого маленького драгоценного камня в вашей системе, перейдите на nodejs.org и нажмите кнопку Install . Когда все закончится, вы можете открыть новую консоль и набрать:

1
npm install -g absurd

Это настроит AbsurdJS глобально. Это означает, что где бы вы ни находились, вы можете выполнить absurd команду.

В мире JavaScript наиболее близким к CSS является формат JSON. Итак, вот что я решил использовать. Давайте рассмотрим простой пример:

1
2
3
4
5
6
7
8
.content {
    padding: 0;
    margin: 0;
    font-size: 20px;
}
.content p {
    line-height: 30px;
}

Это чистый CSS. Вот как это выглядит в LESS и SASS:

1
2
3
4
5
6
7
8
.content {
    padding: 0;
    margin: 0;
    font-size: 20px;
    p {
        line-height: 30px;
    }
}

В контексте AbsurdJS фрагмент должен быть написан так:

01
02
03
04
05
06
07
08
09
10
11
12
module.exports = function(api) {
    api.add({
        ‘.content’: {
            padding: 0,
            margin: 0,
            ‘font-size’: ’20px’,
            p: {
                ‘line-height’: ’30px’
            }
        }
    });
}

Вы можете сохранить это в файл с именем styles.js и запустить:

1
absurd -s .\styles.js

Это скомпилирует JavasSript для того же CSS. Идея проста. Вы пишете пакет NodeJS, который экспортирует функцию. Функция вызывается только с одним параметром — API AbsurdJS. У него есть несколько методов, и я рассмотрю их позже, но наиболее распространенным является add . Он принимает действительный JSON. Каждый объект определяет селектор. Каждое свойство этого объекта может быть свойством CSS и его значением или другим объектом.

Размещение разных частей вашего CSS в разных файлах действительно важно. Такой подход улучшает читаемость ваших стилей. AbsurdJS имеет метод import , который действует как директива @import в препроцессорах CSS.

01
02
03
04
05
06
07
08
09
10
var cwd = __dirname;
module.exports = function(api) {
    api.import(cwd + ‘/config/main.js’);
    api.import(cwd + ‘/config/theme-a.js’);
    api.import([
        cwd + ‘/layout/grid.js’,
        cwd + ‘/forms/login-form.js’,
        cwd + ‘/forms/feedback-form.js’
    ]);
}

Что вам нужно сделать, это написать файл main.js который импортирует остальные стили. Вы должны знать, что есть перезапись. Я имею в виду, что если вы определяете стиль для тега body внутри /config/main.js а затем в /config/theme-a.js используете то же свойство, конечное значение будет тем, которое использовалось в последнем импортированном файле. , Например:

1
2
3
4
5
6
7
8
module.exports = function(api) {
    api.add({
        body: { margin: ’20px’ }
    });
    api.add({
        body: { margin: ’30px’ }
    });
}

Компилируется в

1
2
3
body {
    margin: 30px;
}

Обратите внимание, что есть только один селектор. Хотя, если вы сделаете то же самое в LESS или SASS, вы получите

1
2
3
4
5
6
body {
    margin: 20px;
}
body {
    margin: 30px;
}

Одной из наиболее ценных функций препроцессоров являются их переменные. Они дают вам возможность настроить свой CSS, например, определить настройку где-то в начале таблицы стилей и использовать ее позже. В JavaScript переменные — это нечто нормальное. Однако, поскольку у вас есть модули, размещенные в разных файлах, вам нужно что-то, что действует как мост между ними. Вы можете определить свой основной цвет бренда в одном файле, но позже использовать его в другом. Для этого AbsurdJS предлагает метод API, называемый storage . Если вы выполняете функцию с двумя параметрами, вы создаете пару: key-value . Если вы передадите только ключ, вы получите сохраненное значение.

01
02
03
04
05
06
07
08
09
10
11
12
13
// config.js
module.exports = function(api) {
    api.storage(«brandColor», «#00F»);
}
 
// header.js
module.exports = function(api) {
    api.add({
        header: {
            color: api.storage(«brandColor»)
        }
    })
}

Каждый селектор может принимать не только объект, но и массив. Так что это также верно:

1
2
3
4
5
6
7
8
module.exports = function(api) {
    api.add({
        header: [
            { color: ‘#FF0’ },
            { ‘font-size’: ’20px’ }
        ]
    })
}

Это делает возможным отправку нескольких объектов определенным селекторам. Это очень хорошо сочетается с идеей миксинов. По определению, mixin — это небольшой фрагмент кода, который можно использовать несколько раз. Это вторая особенность LESS и SASS, которая делает их привлекательными для разработчиков. В AbsurdJS миксины на самом деле являются обычными функциями JavaScript. Возможность помещать вещи в хранилище дает вам возможность обмениваться миксинами между файлами. Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// A.js
module.exports = function(api) {
    api.storage(«button», function(color, thickness) {
        return {
            color: color,
            display: «inline-block»,
            padding: «10px 20px»,
            border: «solid » + thickness + «px » + color,
            ‘font-size’: «10px»
        }
    });
}
 
// B.js
module.exports = function(api) {
    api.add({
        ‘.header-button’: [
            api.storage(«button»)(«#AAA», 10),
            {
                color: ‘#F00’,
                ‘font-size’: ’13px’
            }
        ]
    });
}

Результат:

1
2
3
4
5
6
7
.header-button {
    color: #F00;
    display: inline-block;
    padding: 10px 20px;
    border: solid 10px #AAA;
    font-size: 13px;
}

Обратите внимание, что определен только один селектор, а свойство font-size имеет значение второго объекта в массиве (mixin определяет некоторые базовые стили, но позже они изменяются).

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

1
2
3
.header {
    text: medium;
}

Допустим, у нас есть три типа текста: small , medium и big . Каждый из них имеет разный font-size и разную line-height . Очевидно, что я могу добиться того же с помощью миксинов, но AbsurdJS предлагает что-то лучшее — плагины. Создание плагина снова через API:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
api.plugin(«text», function(api, type) {
    switch(type) {
        case «small»:
            return {
                ‘font-size’: ’12px’,
                ‘line-height’: ’16px’
            }
        break;
        case «medium»:
            return {
                ‘font-size’: ’20px’,
                ‘line-height’: ’22px’
            }
        break;
        case «big»:
            return {
                ‘font-size’: ’30px’,
                ‘line-height’: ’32px’
            }
        break;
    }
});

Это позволяет вам применять text: medium к вашим селекторам. Вышеуказанный стиль компилируется в:

1
2
3
4
.header {
    font-size: 20px;
    line-height: 22px;
}

Конечно, библиотека поддерживает медиа-запросы. Я также скопировал идею bubbling функции (вы можете определять точки останова непосредственно внутри элементов, а AbsurdJS позаботится обо всем остальном).

01
02
03
04
05
06
07
08
09
10
11
12
13
api.add({
    ‘.footer’: {
        ‘font-size’: ’14px’,
        ‘@media all and (min-width: 320px) and (max-width: 550px)’: {
            ‘font-size’: ’24px’
        }
    },
    ‘.content’: {
        ‘@media all and (min-width: 320px) and (max-width: 550px)’: {
            margin: ’24px’
        }
    }
})

Результат:

01
02
03
04
05
06
07
08
09
10
11
.footer {
    font-size: 14px;
}
@media all and (min-width: 320px) and (max-width: 550px) {
    .footer {
        font-size: 24px;
    }
    .content {
        margin: 24px;
    }
}

Имейте в виду, что если один и тот же медиа-запрос используется несколько раз, скомпилированный файл будет содержать только одно определение. Это на самом деле экономит много байтов. К сожалению, LESS и SASS не делают этого.

Для этого вам просто нужно передать действительный JSON. В следующем примере показано, как использовать псевдо-классы CSS:

01
02
03
04
05
06
07
08
09
10
module.exports = function(api) {
    api.add({
        a: {
            ‘text-decoration’: ‘none’,
            ‘:hover’: {
                ‘text-decoration’: ‘underline’
            }
        }
    });
}

И это скомпилировано в:

1
2
3
4
5
6
a {
    text-decoration: none;
}
a:hover {
    text-decoration: underline;
}

AbsurdJS работает как инструмент командной строки, но его можно использовать и в приложении NodeJS. Например:

1
2
3
4
5
6
7
8
9
var Absurd = require(«absurd»),
    absurd = Absurd(),
    api = absurd.api,
    output = «./css/styles.css»;
 
api.add({ … }).import(«…»);
absurd.compileFile(output, function(err, css) {
    // do something with the css
});

Или, если у вас есть файл, который действует как точка входа:

1
2
3
4
var Absurd = require(«absurd»);
Absurd(«./css/styles.js»).compileFile(«./css/styles.css», function(err, css) {
    // do something with the css
});

Библиотека также поддерживает интеграцию с Grunt. Вы можете прочитать больше об этом на следующей странице Github .

Доступны три параметра:

  • [-s] — основной исходный файл
  • [-o] — выходной файл
  • [-w] — каталог для просмотра

Например, следующая строка запустит наблюдателя для каталога ./css , возьмет ./css/main.js в качестве точки входа и выведет результат в ./styles.css :

1
absurd -s ./css/main.js -o ./styles.css -w ./css

Не пойми меня неправильно. Доступные CSS-препроцессоры великолепны, и я до сих пор их использую. Тем не менее, они пришли со своим собственным набором проблем. Мне удалось решить их, написав AbsurdJS. Правда в том, что я просто заменил один инструмент другим. Использование этой библиотеки исключает обычное написание CSS и делает вещи действительно гибкими, потому что все является JavaScript. Он может использоваться как инструмент командной строки или может быть интегрирован непосредственно в код приложения. Если вы заинтересованы в AbsurdJS, не стесняйтесь проверить полную документацию на github.com/krasimir/absurd или раскошелиться на репо.