Сегодня я хочу рассказать о том, почему и как мы пришли к использованию препроцессора 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
