Сегодня я хочу рассказать о том, почему и как мы пришли к использованию препроцессора Stylus в разработке Яндекс.Почты, а также описать используемый нами метод работы со стилями для IE. Он очень легко реализуется именно с помощью препроцессоров и делает поддержку IE простой и удобной. Мы разработали для этого специальную библиотеку, которой тоже поделимся — if-ie.styl.
Это только первая статья из серии статей об использовании препроцессора Stylus в Яндекс.Почте, которые мы готовим к публикации.
Как мы пришли к использованию препроцессоров
Хотя внешне Яндекс.Почта выглядит как одностраничное приложение, внутри неё содержится огромное число всевозможных блоков, их модификаций и контекстов, в которых эти блоки и модификации могут оказаться.
Кроме того, у неё уже больше тридцати тем оформления. Есть темы со светлым фоном и с тёмным, есть темы, которые различаются между собой только цветами, а есть и такие, в которых почти весь интерфейс вылеплен из пластилина вручную (http://habrahabr.ru/company/yandex/blog/110556/). В некоторых темах только одно фоновое изображение, а в других фон может меняться — случайно или в зависимости от времени суток и погоды.
Из-за всего этого появляется множество вариаций визуального представления интерфейса, что заставляет чуть иначе относиться к процессу разработки, искать более подходящие к задаче инструменты.
Когда мы только запускали интерфейс «neo2», мы выбрали знакомое нам решение — шаблонизатор Template Toolkit 2, с несколько нестандартным сценарием его использования для генерации CSS, а не HTML. Поначалу нам были нужны только переменные, но со временем темы усложнялись, и в итоге оказалось, что такой инструмент неудобен. Громоздкий синтаксис, отсутствие специализированных под CSS функций и общее чувство использования инструмента не по назначению заставили искать другие варианты. Мы поняли, что нам не обойтись без препроцессора.
Выбор препроцессора
Выбирали между тремя вариантами: Sass, Less и Stylus. Процесс был довольно простым: мы взяли несколько имеющихся блоков, после чего попробовали переверстать их, используя каждый из препроцессоров.
Less показался поначалу очень простым и удобным: он использует привычный синтаксис CSS и его можно применять в браузере, что удобно для отладки. Но при попытке сделать что-то сложное его возможностей уже не хватает. Что можно сделать без массивов, циклов и нормальных условных конструкций? Не так много.
После Less Sass оказался очень приятным: мощный, с хорошим сообществом и всевозможными дополнительными библиотеками вроде Compass. Однако нашёлся один серьёзный недостаток: в Sass очень негибкий parent reference (использование символа &
в селекторах для указания на родительский селектор). Из-за этого возникает несколько проблем, и все они заключаются в том, что &
нельзя использовать для префиксного определения множественных классов, уточнения элемента или конкатенации классов. Вот несколько примеров того, что умеют другие препроцессоры, но что может вызвать ошибку в Sass:
&__bar
, применённое для селектора.foo
, должно давать.foo__bar
— подобные конструкции нужны для упрощения использования БЭМ-наименования и очень удобны когда нужно сгенерировать в цикле множество модификаторов..baz&
, применённое для селектора.foo .bar
, должно дать мультикласс.baz.foo .bar
, но так в Sass сделать не получится: можно будет дать мультикласс только к.bar
, если написать&.baz
, но не наоборот.button&
, применённое к.foo
, должно бы уточнить селектор доbutton.foo
, но — увы.
И всё было бы не так плохо, если бы в Sass можно было использовать указатель на родительский селектор в интерполяции. Но нет — Sass сначала разворачивает интерполяцию и только потом пытается применить селектор, выдавая ошибку.
Обиднее всего то, что подобное поведение — не баг, а фича. И меняться это не будет. Такая уж идеология у синтаксиса Sass.
Stylus оказался самым новым из препроцессоров, и в конце концов наш выбор пал на него. Да, он сыроват, в нём встречаются неприятные баги, его комьюнити не такое большое и разработка идёт не так быстро, как того хотелось бы. Но для наших задач он подошёл лучше всего, и вот почему:
- Stylus — очень гибкий препроцессор, и часто он оказывается гораздо гибче того же Sass. Например, parent references Stylus раскрывает идеально.
- В Stylus есть такая штука как прозрачные миксины — возможность вызвать функцию как обычное CSS-свойство. То есть сначала определить
foo(bar)
, а потом вызвать её какfoo: 10px
. Такая запись будет равнозначна вызовуfoo(10px)
. Для обычных функций это не всегда удобно, но зато позволяет переопределить любое имеющееся свойство. Скажем, можно переопределитьmargin
вpadding
. Если серьёзно, то при использовании подобной функциональности можно легко запутаться и усложнить понимание того, что же делает код: ведь не всегда будет ясно, какие функции были определены выше по коду и что произойдёт в результате вызова очередного свойства.Однако прозрачные миксины очень сильно упрощают поддержку, например, браузерных префиксов. Не нужно запоминать какие префиксы имеет то или иное свойство и заботиться о том, нужно ли для очередного свойства писать специальную конструкцию. Достаточно просто всегда писать без префиксов, а об их добавлении позаботится подключённая библиотека (мы используем для этого свой форк nib.
- Stylus написан на JS. Это значит, что его проще поддерживать и править в нём баги (все разработчики интерфейса Яндекс.Почты гораздо лучше знают JS, чем Ruby). К тому же это позволяет проще использовать Stylus в цепочке с другими инструментами на node.js (например, CSSO).
В Stylus есть ещё много очень разных полезных вещей, но именно приведённые выше заставили нас сделать выбор в его пользу.
Конечно, кроме преимуществ, у Stylus есть и недостатки. И основной из них — гибкий синтаксис — авторы препроцессора считают его главным достоинством. Погнавшись за гибкостью, они целиком реализовали только синтаксис, основанный на отступах, тогда как вариант «а-ля CSS» кое-как прикручен сверху и не получится просто так взять и переименовать .css
в .styl
— не все варианты написания CSS заработают и в Stylus. Но мы решили, что возможности, которые даёт нам этот препроцессор, делают его недостатки не такими значительными, поэтому пришлось смириться с некоторой капризностью парсера (и начать использовать синтаксис, основанный на отступах).
Подытоживая рассказ про выбор, стоит отметить, что Sass и Stylus — два почти равнозначных варианта. Каждый из них имеет как свои преимущества и уникальные фичи, так и недостатки. Если вы уже используете какой-то из этих препроцессоров и вас всё устраивает — отлично, можно не думать о поиске нового. Но если вы только подходите к выбору или же с используемым препроцессором вам становится тесно, попробуйте сравнить все варианты. Лучший способ это сделать — примерить каждый препроцессор к своей задаче. Сверстав часть вашего проекта на каждом из препроцессоров, вы поймёте, какие их возможности вам важны, а какие — нет. Только не забывайте, что препроцессор — это не просто другой синтаксис, но и другой подход: при подобной перевёрстке можно заодно и отрефакторить код, сделав что-то оптимальнее, чем было с простым CSS.
Библиотека if-ie.styl
Во время перевода Яндекс.Почты на Stylus стало понятно, что препроцессор может предоставить возможности, которых у нас не было при использовании обычного CSS. Мы давно использовали в Почте разделение стилей для обычных браузеров, и для старых версий IE. Все стили раскладывались по файловой структуре согласно БЭМ, и для каждого блока рядом создавалось два файла: b-block.css
и b-block.ie.css
.
В первый файл шли стили для всех браузеров, а во второй, соответственно, только те, что были нужны для Internet Explorer. Тут надо отметить, что по ряду причин мы переводим IE8 в режим совместимости с IE7, а также, перестав поддерживать IE6, отправляем его на «облегчённую» версию Почты. Таким образом, у нас есть две разных версии стилей: одна для всех браузеров, вторая — для всех старых версий IE. Всё собиралось автоматически — сначала собиралась таблица стилей «для всех», после чего к ней добавлялись все стили из файлов .ie.css
— и получалась таблица стилей для IE.
Каждый браузер получает только свою таблицу стилей — для этого мы используем условные комментарии, примерно так:
<!--[if gt IE 7]><!-->
<link rel="stylesheet" href="style.css" />
<!--<![endif]--><!--[if lt IE 8]>
<link rel="stylesheet" href="style_ie.css" />
<![endif]-->
Поначалу подобное разделение стилей работало хорошо, однако, с появлением препроцессора возник вопрос: а нельзя ли сделать лучше?
Оказалось, что можно.
Так появилась библиотека для Stylus — «if-ie.styl». На самом деле это не совсем библиотека, скорее — методология того, как нужно работать со стилями для IE. Ничего секретного в этой методологии нет, поэтому мы решили выложить её на Гитхаб под лицензией MIT. Вы можете совершенно спокойно использовать её для своего проекта, сообщать о найденных ошибках или и даже самостоятельно их исправлять — Open Source, все дела. А может быть, у кого-то появится возможность переписать её для другого препроцессора? Форкайте проект или создавайте новый, взяв методологию за основу — будет здорово.
Основа методологии
В основе методологии лежит очень простая идея: мы создаём переменную ie
, которая будет равна false
для всех обычных браузеров и true
, когда мы захотим получить стили для IE. После этого можно использовать только одну основную таблицу стилей, в которой разграничивать стили для разных браузеров обычными условиями. Простой пример: возьмём файл style.styl
:
ie ?= false
.foo
overflow: hidden
zoom: 1 if ie
Такой код на Stylus по умолчанию — для обычных браузеров — станет вот таким CSS:
.foo {
overflow: hidden;
}
Так как переменная ie
была false
, условие if ie
не сработало, и свойство zoom
не вошло в эту таблицу стилей.
Теперь создадим рядом style_ie.styl
:
ie = true
@import style.styl
Если скормить такой код Stylus, то мы получим:
.foo {
overflow: hidden;
zoom: 1;
}
Это именно то, что нам и требовалось — отдельная таблица, в которой есть стили, нужные только в IE.
При этом, если мы захотим написать какие-то стили только для обычных браузеров, мы сможем использовать условие if !ie
— и фильтровать стили станет очень просто.
Дополнительные возможности if-ie.styl
Конечно же, этого нам показалось мало и мы решили добавить всяких полезных функций, которые бы оптимизировали многое в нашем коде. Так и получилась «библиотека».
Для её использования подключение стилей будет выглядеть чуть иначе.
Файл style.styl
станет таким:
@import if-ie.styl
.foo
overflow: hidden
zoom: 1
A style_ie.styl
таким:
ie = true
@import if-ie.styl
@import style.styl
Отличий, в сравнении с вариантом без дополнительных функций, два:
- В обоих файлах нужно подключать библиотеку до всех остальных стилей, и обязательно оба раза. (Может показаться, что хватило бы только первого — ведь таблица стилей для IE уже содержит основную таблицу стилей — но тогда некоторые последующие возможности не будут работать). В этом файле уже определяется
ie ?= false
, и поэтому в основной таблице стилей не нужно это явно прописывать. - В таблице стилей для IE пропало условие
if ie
— это показана работа первой фичи библиотеки: свойствоzoom
автоматически появится только в таблице стилей для IE. Конечно, и в обычных браузерах можно использовать это свойство, но на практике такая необходимость случается крайне редко, так что можно облегчить основную таблицу стилей хотя бы чуть-чуть.
В коде Stylus подобные функции очень легко определять. В данном случае новый прозрачный миксин будет выглядеть так:
zoom()
zoom: arguments if ie
Этим кодом мы создаём функцию zoom
, которая пропишет соответствующее свойство с любыми переданными аргументами только в том случае, если мы в данный момент собираем стили для IE.
inline-block
Широко известно, что в старых версиях IE у свойства display
«из коробки» нет поддержки значения inline-block
. В IE есть аналогичный механизм, но он срабатывает только для инлайновых элементов и только если у них включён механизм hasLayout. Совершенно случайно display: inline-block
в IE как раз его включает, но «забывает» переключить элемент в display: inline
, поэтому подобная запись не будет работать для изначально блочных элементов вроде <div>
. Так что наиболее простой способ гарантированно применить инлайн-блочное поведение к любому элементу в IE — прописать только для него zoom: 1; display: inline;
.
В if-ie.styl свойство display
так будет делать автоматически и только для IE. Обычные браузеры увидят display: inline-block
, тогда как IE получит только zoom: 1; display: inline;
. Тут кто-то мог бы заметить, что вторая запись длиннее первой, и для изначально инлайновых блоков можно было бы и сэкономить… Но если внимательно подсчитать, то экономия окажется всего в один байт. Это не стоит того, чтобы всё время следить — инлайновый ли блок изначально или нет. Тем более что в идеале вёрстка не должна зависеть от того, к какому элементу она применяется.
Переопределение CSS3-свойств
Если мы по умолчанию не отдаём обычным браузерам zoom: 1
, разделив все стили на два файла (как было, в общем-то, и до внедрения препроцессоров), то с препроцессорами можно задуматься и над тем, как облегчить и таблицу стилей для IE.
С помощью Stylus это делается очень просто, при этом можно сэкономить довольно много байт: ведь что нам точно в старых IE не нужно, так это свойства с префиксами.
Выше в статье я уже упомянул, что мы используем библиотеку nib — эта библиотека определяет прозрачные миксины для большинства новых CSS-cвойств, например, для транзишнов. В итоге в стилях для Stylus мы пишем просто transition: opacity .3s
и в результате получаем это свойство со всеми нужными префиксами. Но в IE нам не только префиксы не нужны, но и само свойство! А значит, мы можем в нашей библиотеке переопределить это и многие другие свойства так, чтобы они ничего не возвращали.
Делается это примерно так:
if ie
transition()
z if 0
transition-property()
z if 0
// …
Тут всё почти понятно: одним условием мы только для IE переопределяем сразу все нужные свойства. Однако из-за особенностей Stylus приходится в определении функции написать хотя бы одно правило — z if 0
получается довольно коротким.
В итоге IE вместо определённого в nib миксина transition
видит тот, что мы определили в if-ie.styl, и полученная таблица стилей окажется гораздо легче, чем раньше — почти всё ненужное из неё будет вырезано.
rgba-ie
Не будем раздувать статью и описывать всё, что есть в if-ie.styl — в документации к проекту это уже подробно описано.
Однако нужно рассказать ещё про одну функцию, которая оказалась нам очень полезна в рамках тематизации Яндекс.Почты. Это функция rgba-ie
. На самом деле эта функция могла бы называться просто rgba
, но в Stylus есть баг: функции, определённые в JS, не получается переопределять так же, как те, что были определены в Stylus, так что тут пришлось создать новую.
Что же она делает? Старые IE не поддерживают значения цвета, заданные в формате rgba. Поэтому обычно разработчики либо прописывают соответствующие цвета дважды — сначала для старых IE в обычном hex-формате, а потом уже всем нормальным браузерам в желаемом rgba
— либо используют modernizr и уже с помощью него и класса .rgba
задают соответствующие цвета там, где это нужно. Но для фолбеков в IE каждый раз всё равно приходится вычислять примерный цвет того, во что мы будем в нём деградировать. Чаще всего это будет нужный цвет, наложенный поверх фона страницы или среднего фона элемента, над которым будет применён цвет в rgba
.
Функция rgba-ie
из if-ie.styl сильно упрощает эту задачу: дублируя возможности обычной функции rgba
, мы получаем ещё один опциональный параметр, который можно передать в функцию — цвет фона для фолбека. По умолчанию этот параметр задан в #FFF
.
Простой пример:
.foo
color: rgba-ie(0,0,0,0.5)
В обычных браузерах этот цвет будет обычным rgba(0,0,0,0.5)
, но в IE он превратится в #808080
— то есть в соответствующий цвет, наложенный поверх белого.
Более сложный пример, с целевым фоном в качестве последнего аргумента (и с использованием одной из фич Stylus — возможности указать вместо трёх цифр r
, g
и b
цвет в hex):
.foo
background: rgba-ie(#FFDE00, .42, #19C261)
В этом примере для нормальных браузеров будет цвет rgba(255,222,0,0.42)
, а вот IE получит правильный #7ace38
.
При этом есть возможность задать и фолбек по умолчанию с помощью переменной $default_rgba_fallback
.
В итоге можно очень сильно упростить себе жизнь, если использовать функцию rgba-ie
вместо обычного rgba
— об IE в этом случае можно будет почти не вспоминать.
Автор: kizu