Приводим в порядок css-код. Опыт Яндекса

Содержание

Всем привет!

Я работаю над фронтендом огромного проекта — поисковой выдачи Яндекса. И у нас, как и у любого другого большого веб-проекта, есть огромное количество css-кода и немаленькая команда, которая с ним взаимодействует.

Когда много людей, используя разные инструменты, пишут и редактируют css, со временем этот css может получиться очень запутанным, неконсистентым и в целом начинает выглядеть плохо. Например, кому-то удобнее писать вендорные префиксы в одном порядке, кому-то — в другом, кто-то ставит кавычки вокруг url, кто-то — нет, а кто-нибудь фикся срочную багу к релизу мог бы, к примеру, написать position: relative в начале блока свойств, незаметив что где-нибудь внизу между color и box-shadow, уже есть position: absolute, и долго гадать, почему у него ничего не работает.

9bef109258aff563f8fcff4a3177ccb1

Но несмотря на то, что все пишут код по-разному, у нас в репозитории идеальный порядок: css-код полностью консистентен, и прекрасно выглядит. Весь. 

Как мы этого добились, можно прочитать под катом.

Первые шаги


Если мы хотим, чтобы весь css писался по кодстайлу, то первая очевидная мысль — это чтобы какой-нибудь самый-главный-начальник сказал: «Надо писать так-то и так-то, соблюдайте», а несоответствия отлавливать на код ревью. Звучит круто, но, к сожалению, только звучит, а на практике получается так себе:

  1. Все мы люди — а человеку немного сложно запомнить точный-рекомендованный-порядок всей кучи css свойств, и никогда в нем не ошибаться.
  2. Код ревью в таком случае превращается в набор правок по кодстайлу, а хотелось бы чтобы в нем обсуждали логику и архитектуру.
  3. Все ошибки в этом месте нужно исправлять руками, а все несоответствия уточнять в какой-нибудь доке.


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

Немного о роботах


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

  1. Кроме этого он не умел ничего, а ведь очень хотелось и кавычки где не надо убирать, и вендорные префиксы лесенкой выстраивать…
  2. Был написан на PHP и регэкспах, а также не предусматривал расширения.


И хотя, конечно, PHP в среде js-программистов немного известнее чем c++, это все-таки не совсем то, чего бы хотелось.

mishanga из нашей команды, посмотрел на все это дело и решил сделать инструмент, который был бы лишен этих недостатков: умел много разных штук, был бы написан на js, и мог легко расширяться. И хотя сотрудники Яндекса принимают активное участие в разработке, CSScomb.js — полноценный опенсорсный проект, давно вышедший за рамки Яндекса. 

Знакомьтесь, CSScomb.js — это новая версия старого CSScomb, умеющая кучу всяких крутых вещей.

Как это выглядит ?


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

.block {
-webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
-moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
z-index: 2;
position: absolute;
height: 2px;
width: 2px;
color: #FFFFFF;
}


Выглядит он не очень, и вообще не по кодстайлу, и поэтому при попытке коммита мы увидим вот что:

% git commit block.css -m"Add My awesome css"                                                                                   
Code style errors found.
! block.css


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

Увидев такое сообщение, мы не расстраиваемся, а просто просим CSScomb.js все поправить:

% csscomb  block.css


Вот, теперь другое дело:

.block
{
    position: absolute;
    z-index: 2;

    width: 2px;
    height: 2px;

    color: #fff;
    -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
       -moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
            box-shadow: 0 0 0 1px rgba(0, 0, 0, .1);
}


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

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

А внутре у ней неонка


Такая система состоит из двух частей — собственно CSScomb-а и прекоммит-хука который его использует. Про хук я расскажу в следующем разделе, а в этом подробно остановлюсь на том, как устроен CSSComb.js

В основе CSScomb.js две вещи: система плагинов и css-парсер gonzales-pe — очень крутая штука, которая умеет работать не только с чистым css, но и препроцессорами вроде Sass или LESS.

Начнем с парсера. Он натравливается на css-код и строит на его основе AST. Например, для:

.block
{
     position: absolute
}


AST будет выглядеть так:

[ "stylesheet",
  [ "ruleset",
    [ "selector",
      [ "simpleselector",
        [ "class", [ "ident", "block" ] ],
        [ "s", "\n" ]
      ]
    ],
    [ "block",
      [ "s", "\n    " ],
      [ "declaration",
        [ "property", [ "ident", "position" ] ],
        [ "propertyDelim" ],
        [ "value", [ "s", " " ], [ "ident", "absolute" ] ]
      ],
      [ "s", "\n" ]
    ]
  ],
  [ "s", "\n" ]
]


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

В примере выше мы не поставили точку с запятой после absolute. Плагин, который отвечает за детектирование и исправление точек с запятыми, это заметил и исправил через модификацию AST(я немного сократил дерево чтобы не дублировать большой кусок кода):

...
    [ "block",
      [ "s", "\n    " ],
      [ "declaration",
        [ "property", [ "ident", "position" ] ],
        [ "propertyDelim" ],
        [ "value", [ "s", " " ], [ "ident", "absolute" ] ]

      ],
      >>>[ "declDelim" ],<<<
      [ "s", "\n" ]
...


После того как отработали все плагины, c помощью того же gonzales-pe AST превращается обратно в css код,
и забытая точка с запятой оказывается на своем месте:

.block
{
     position: absolute;
}

 

Как начать это использовать ?

 

Шаг первый


Нужно тем или иным способом добавить CSScomb.js в зависимости своего проекта. 
В случае если на проекте используется npm, нужно добавить его в devDependencies в package.json

{
  ...
  "devDependencies": {
    ...
    "csscomb": "2.0.4",
    ...
  }
  ...
}

 

Шаг второй


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

CSScomb.js до какой-то степени может это сделать автоматически, потому что умеет генерировать конфиг, взяв за основу какую-нибудь существующую css-ку.

csscomb -d example.css > .csscomb.json


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

Шаг последний


Нужно сделать так, чтобы CSScomb.js в режиме линтера запускался перед коммитом наравне с jscs/jshint-groups или другими инструментами, которые обычно запускаются перед комитом. В случае использования git и linux/BSD он может выглядеть примерно так:

#!/usr/bin/env bash

PATCH_FILE="working-tree.patch" 
NPM_BIN="./node_modules/.bin"

function cleanup {
    exit_code=$?
    if [ -f "$PATCH_FILE" ]; then
        patch -p0 < "$PATCH_FILE"
        rm "$PATCH_FILE"
    fi
    exit $exit_code
}

#Прибираемся при выходе из скрипта
trap cleanup EXIT SIGINT SIGHUP

# Создаем  патч
git diff > "$PATCH_FILE" --no-prefix
# Сбрасываем не застэйдженный изменения
git checkout -- .

# получаем список файлов в которых были изменения, которые мы хотим закоммитить
git_cached_files=$(git diff --cached --name-only --diff-filter=ACMR | xargs echo)
if [ "$git_cached_files" ]; then
    #Собственно натравливаем CSScomb.js
    $NPM_BIN/csscomb -v -l $git_cached_files || exit 1
fi


У этого хука есть интересная особенность. Казалось бы, почему не запускать CSSComb.js сразу в режиме исправления и затем автоматически молча коммитить? В большинстве случаев это действительно нормально работает, но проблемы возникают в ситуации, когда мы делаем несколько правок в файле, а закоммитить хотим только одну из них (git add -p). В этом случае если мы находим ошибку в той версии файла которую коммитим, начинаются неприятные ситуации:

  1. Мы можем запустить CSScomb.js на ту версию файла, которую собираемся закоммитить, и в некоторых ситуациях получить конфликты при наложении патча.
  2. Либо запустить CSScomb.js на текущем файле целиком, но в правках, которые мы пока не хотим коммитить, может быть что угодно, включая неправильный код.


Да и в целом это не очень приятно, когда файл неожиданно сам по себе меняется. Поэтому мы пришли к выводу, что CSScomb.js должен запускаться самим разработчиком.

Готово!


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

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