Компания
376,43
рейтинг
9 июля 2014 в 16:05

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

Всем привет!

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

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



Но несмотря на то, что все пишут код по-разному, у нас в репозитории идеальный порядок: 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, либо законтрибьютить нужные штуки самостоятельно.
Автор: @Beyondtheclouds
Яндекс
рейтинг 376,43

Комментарии (70)

  • +8
    Вы используете исключительно ванильный CSS, без LESS, SASS, etc.?
    • 0
      Судя вот по таким штукам в документации, не только.
      • +7
        Конкретно на нашем проекте пока ванильный css, да. но CSSComb.js вполне умеет работать и c SASS/LESS
        • +3
          Когда ждать поддержки stylus?
    • +4
      Попробовал на less в своем проекте — очень красиво все сделало.
      Что интересно, форматирует даже закомментированные свойства.
    • +1
      С SASS работает прекрасно.
      С недавних версий вполне отлично дружит с Compass.
      • 0
        А у меня загнулось.
        На переменных вначале файла где у меня цвета забиты ругается дальше не идет.
        Убрал их, всю структуру стерло и отформатировало как обычный CSS.
        Чего то не хватает.
        А CSS отформатировало прекрасно, хотя вещается на комментариях начинающиеся с //
        • 0
          Абсолютно та же ситуация была. Проверьте, что ставите последний csscomb – ранние версии глючили с Compass. С версией 3.0.0-5 полет нормальный.
          • 0
            хм переключился на третью ветку, стало получше.
            но на переменных все одно падает, пока не все тянет.
            • 0
              А покажите код, на котором падает, пожалуйста? github.com/csscomb/csscomb.js/issues/new
              • 0
                Ок, сейчас попробую локализовать проблемные места и закину.
              • 0
                Простая декларация переменных в SCSS, все остальное вроде работает на ура…

                github.com/csscomb/csscomb.js/issues/275
        • +2
          `// comment` = невалидный комментарий для css, поэтому появляется ошибка парсинга.
          • 0
            Да, но блин часто используемый. Сейчас занимаюсь системой на JBoss, тут вебмастеров отродясь похоже не было, столько соплей и не валидной разметки я с прошлого века не видел.
            Вот будут потихонечку все это переписывать и приводить к кошерному виду, пока все под Section 508 привожу.
  • 0
    Копаться в css — както неблагородно в наши дни.
    Особенно, когда проект размером с самолет.

    Для систематизации и оптимизации отлично подойдут функции, миксины и прочие крутые штуки, которые предалагает, например, LESS.
    • +6
      Использование препроцессоров не отменяет вопрос группировки и сортировки свойств. Опять-таки, нужно будет определить, где необходимо инклюдить те же миксины.
  • +5
    Имеется ли у вас общедоступный стайлгайд по стилям, который включает порядок свойств, вендорных префиксов, необходимые отступы?
  • 0
    Есть ли возможность обойти хук? В редких случаях такое нужно.
    Можно было бы сделать через комментарий в начале файла вида /* @CSSComb ignore */
    • +7
      Если ты мегаотвественный и понимаешь что делаешь то ты можешь сделать.
      git commit ololo --no-verify
      Но вся отвественность за такое будет полностью на тебе.
      • +1
        по моему важно, чтобы этот коммит потом получил отлуп и на pre-receive хуке на сервере…
        • +4
          Часто бывает, что инструменты некорректно воспринимают код и ругаются, даже если он правильный. В этом случае у разработчика должна быть возможность сознательно обойти автоматическую защиту и закоммитить код.
          Мы верим, что люди умнее программ :)
  • +1
    Почему AST имеет форму массива (а не объекта, как это сделано в Mozilla Parser API)?.
    • 0
      мб потому что простого списка достаточно?
      • +2
        Но ведь эта достаточность подчас стесняет.

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

        Кроме того, в массивах хуже поддерживается обратная совместимость. Получается, что на будущее нельзя отказаться ни от одного из элементов массива без того, чтобы не принудить пользователей ставить на его место всегда «null» (или что-то другое в этом же дýхе) «по историческим соображениям».
        • –1
          это не просто массив, а синтаксическое дерево… вроде лиспа…
          • +3
            Повторяю первоначальный вопрос: почему AST (это сокращение означает «abstract syntax tree», так что я знал с сáмого начала, что это синтаксическое дерево) имеет форму массива, а не объекта?
            • +2
              Это больше вопрос к автору парсера: github.com/tonyganch/gonzales-pe.
              Достоверно, почему сделанно именно так, я ответить не могу. Но всегда можно создать issue и спросить там.
        • +1
          Массивы — компактные и на порядок производительнее. У нас также есть один парсер, и там мы боримся с каждой мс. Сейчас у нас дерево через обьекты представлено, a вот однажды пытались на массивы перейти. И всё было очень здорово — с индексами было удобно работать через константы:
          // INode [type:int, tag:string, attributes:object, nodes:array]
          var tagName = node[pos_TAG];
          var children   =  node[pos_NODES];
          

          В конечном разультате код стал быстрее и после минификации стал меньше. Но пришлось отказаться — хотелось давать конечным пользивателям непосредственный доступ к дереву. А вот там ему уже эти индексы нужно помнить, ведь `pos_*` скрыты внутри библиотеки, а выносить как свойства наружу — усложнит простую работу с деревом.

          Хотя заметили, что в csscomb другая структура используется: [KEY, VALUE] и такой подход немного перемешивает массивы с обьектами; как по мне — довольно не корректно.
    • +2
      Массив упорядоченный, а объект, вроде как, не обязательно?
      • 0
        Да.

        Но является ли упорядоченность массива достоинством или недостатком?

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

        Если свойство объекта выйдет из употребления, то достаточно перестать употреблять его — а если элемент массива выйдет из употребления, то всех пользователей новых версий придётся приучить на его место в массиве ставить «null» (или что-то другое в этом же роде) «по историческим соображениям», что и выглядит похуже, и время растрачивает.
        • +1
          В описанном случае достаточно ввести новый тип узла.
  • +1
    Есть ли какая-нибудь возможность задать такой формат?
    .block {
        position: absolute;
    }
    

    Всё, что у меня получается, — это либо открывающая скобка на новой строке, либо закрывающая с отступом.
    • +3
      Попробуйте в конфигурации опцию space-before-opening-brace установить в " "
      • 0
        Уже пробовал. Никак не реагирует.
      • 0
        Извиняюсь, всё заработало. Нужно было указать "space-before-closing-brace": "\n", почему не срабатывало раньше — не понимаю.
  • +5
    Скажите, а зачем вы сами пишите вендорные префиксы, если есть такие прекрасные проекты, как autoprefixer?
    • +6
      Очень пристально на него смотрим и планируем использовать :)
    • +1
      Редко, но бывает, что какие-нибудь хитрые случаи приходится костылять явно.
      Но в целом, конечно, Автопрефиксер прекрасен :)
      • 0
        По идее есть api для своих префиксов
  • 0
    del
  • –1
    «мог бы, к примеру, написать position: relative в начале блока свойств, незаметив что где-нибудь внизу между color и box-shadow, уже есть position: absolute, и долго гадать, почему у него ничего не работает» — было недавно
    • +3
      Вы в блокноте что ли стили пишете? Найдите редактор получше, который такое дело сразу видит и дает вам знать.
  • +1
    верните нумерацию выдачи в поиске
  • 0
    a {
    	position: relative;
    
    	color: #a00;
    	span {
    		display: block;
    		float: left;
    	}
    }
    

    можно ли как-то добавить пустую строку между цветом и спаном?
    • 0
      Можно группировать как душе угодно: github.com/csscomb/csscomb.js/blob/master/doc/options.md#sort-order
      • 0
        да, это понятно, $variable — переменные, $include — миксины…
        но как обозначаются вложенные правила (селекторы)?
        • +1
          Вложенные правила сейчас никак не сортируются. Есть вот такая похожая задача: github.com/csscomb/csscomb.js/issues/210
          Но если color входит в список sort-order, пустая строка должна была добавиться. Заведите таск, пожалуйста, я посмотрю: github.com/csscomb/csscomb.js/issues/new
  • 0
    Раньше на страничке проекта был плагин для Coda 2, сейчас вместо всего сайта просто заглушка. Скажите, где можно скачать плагин и планируется ли его поддержка в будущем?
    • 0
      На первую часть вопроса нашел ответ
      • 0
        Поддержка плагина для Coda 2 не планируется.
  • 0
    Вот вы описали случай, когда в одном блоке указаны 2 свойства position. В одном месте absolute, в другом — relative. Что сделает с этими свойствами CSSComb?
    • 0
      Должен их рядом сгруппировать по тому, порядку в котором они встретились в коде
      • 0
        А выведет ошибку или нет? Да ладно, что это я, сейчас просто сам возьму да и попробую.
        • +2
          Нет, это же не ошибка, а вполне себе валидный CSS-код.
          Пишут ведь костыли вида "display: inline-block; display: inline;" для старых браузеров.
  • 0
    Код-стайл – это, конечо, важно, но что делать, если проблемы более глобальны?
    Вот мне сейчас достался проект, где куча мертвого кода, костылей, переопределяющих всё и вся и прочей ереси. Я к нему подступиться боюсь.
    • +5
      Я незнаю инструмента который мог бы помочь в этой ситуации кроме кофе )
      • +1
        Много кофе и переверстать. :)
        И сразу привить нормальный стиль.
        Вобще, у меня есть ощущение, что если не следить за стилем, то CSS в помойку превращается раньше других частей и необратимее.
        • 0
          Очень спасает модульность. Когда кода много, он должен быть разбит на отдельные понятные куски. Например, по принципам БЭМ.
          • 0
            И чем модульность поможет от мертвых стилей?
            • +1
              По принципу сборки икеевской мебели: всё, что осталось лишним после сборки, можно выкинуть.
              Если перетряхнуть старый код и разгрести на кучки, останутся лишние куски, которые с высокой вероятностью и окажутся лишними.
              • 0
                А как вы узнаете что лишнее? Вдруг у вас есть какая-то страничка, про которую все благополучно забыли, а эти стили там используются. Ну или какой-нибудь элемент на странице при определенных условиях принимает определенный стиль.
                • +1
                  Ну я даже не знаю… Протестировать? :)
                  Для этого есть unit-тесты на скрипты и вёрстку, есть регрессионные авто-тесты.
                  В самом простом случае можно погрепать проект и поискать там упоминание оставшихся стилей.

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

                  Ну или можно пойти другим путем: сказать себе, что навести порядок невозможно, и уволиться ;)
          • 0
            Это когда она есть. А когда её нет, то не спасает… :)
            • 0
              Чтобы модульность спасла, её нужно внедрить. См. выше :)
    • +3
      github.com/csslint/csslint + аудит в Хроме
      • +1
        Вот спасибо! Посмотрим.
  • 0
    Можно попробовать обойтись без bash-файлов с помощью gulpjs+npm scripts+husky github.com/matmuchrapna/webapp-base-linting/
    • 0
      сделал ещё проще c помощью gulp + vinyl-git + husky

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

Самое читаемое Разработка