Сегодня я хочу рассказать о том, почему и как мы пришли к использованию препроцессора 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 можно было использовать указатель на родительский селектор в интерполяции. Но нет — Sass сначала разворачивает интерполяцию и только потом пытается применить селектор, выдавая ошибку.
Обиднее всего то, что подобное поведение — не баг, а фича. И меняться это не будет. Такая уж идеология у синтаксиса Sass.
Stylus оказался самым новым из препроцессоров, и в конце концов наш выбор пал на него. Да, он сыроват, в нём встречаются неприятные баги, его комьюнити не такое большое, и разработка идёт не так быстро, как того хотелось бы. Но для наших задач он подошёл лучше всего, и вот почему:
В Stylus есть ещё много очень разных полезных вещей, но именно приведённые выше заставили нас сделать выбор в его пользу.
Конечно, кроме преимуществ, у Stylus есть и недостатки. И основной из них — гибкий синтаксис — авторы препроцессора считают его главным достоинством. Погнавшись за гибкостью, они целиком реализовали только синтаксис, основанный на отступах, тогда как вариант «а-ля CSS» кое-как прикручен сверху, и не получится просто так взять и переименовать
Подытоживая рассказ про выбор, стоит отметить, что Sass и Stylus — два почти равнозначных варианта. Каждый из них имеет как свои преимущества и уникальные фичи, так и недостатки. Если вы уже используете какой-то из этих препроцессоров и вас всё устраивает — отлично, можно не думать о поиске нового. Но если вы только подходите к выбору или же с используемым препроцессором вам становится тесно, попробуйте сравнить все варианты. Лучший способ это сделать — примерить каждый препроцессор к своей задаче. Сверстав часть вашего проекта на каждом из препроцессоров, вы поймёте, какие их возможности вам важны, а какие — нет. Только не забывайте, что препроцессор — это не просто другой синтаксис, но и другой подход: при подобной перевёрстке можно заодно и отрефакторить код, сделав что-то оптимальнее, чем было с простым CSS.
Во время перевода Яндекс.Почты на Stylus стало понятно, что препроцессор может предоставить возможности, которых у нас не было при использовании обычного CSS. Мы давно использовали в Почте разделение стилей для обычных браузеров и для старых версий IE. Все стили раскладывались по файловой структуре согласно БЭМ, и для каждого блока рядом создавалось два файла:
В первый файл шли стили для всех браузеров, а во второй, соответственно, только те, что были нужны для Internet Explorer. Тут надо отметить, что по ряду причин мы переводим IE8 в режим совместимости с IE7, а также, перестав поддерживать IE6, отправляем его на «облегчённую» версию Почты. Таким образом, у нас есть две разных версии стилей: одна для всех браузеров, вторая — для всех старых версий IE. Всё собиралось автоматически — сначала собиралась таблица стилей «для всех», после чего к ней добавлялись все стили из файлов
Каждый браузер получает только свою таблицу стилей — для этого мы используем условные комментарии, примерно так:
Поначалу подобное разделение стилей работало хорошо, однако с появлением препроцессора возник вопрос: а нельзя ли сделать лучше?
Оказалось, что можно.
Так появилась библиотека для Stylus — «if-ie.styl». На самом деле это не совсем библиотека, скорее — методология того, как нужно работать со стилями для IE. Ничего секретного в этой методологии нет, поэтому мы решили выложить её на Гитхаб под лицензией MIT. Вы можете совершенно спокойно использовать её для своего проекта, сообщать о найденных ошибках или и даже самостоятельно их исправлять — Open Source, все дела. А может быть, у кого-то появится возможность переписать её для другого препроцессора? Форкайте проект или создавайте новый, взяв методологию за основу — будет здорово.
В основе методологии лежит очень простая идея: мы создаём переменную
Такой код на Stylus по умолчанию — для обычных браузеров — станет вот таким CSS:
Так как переменная
Теперь создадим рядом
Если скормить такой код Stylus, то мы получим:
Это именно то, что нам и требовалось — отдельная таблица, в которой есть стили, нужные только в IE.
При этом, если мы захотим написать какие-то стили только для обычных браузеров, мы сможем использовать условие
Конечно же, этого нам показалось мало и мы решили добавить всяких полезных функций, которые бы оптимизировали многое в нашем коде. Так и получилась «библиотека».
Для её использования подключение стилей будет выглядеть чуть иначе.
Файл
A
Отличий, в сравнении с вариантом без дополнительных функций, два:
В коде Stylus подобные функции очень легко определять. В данном случае новый прозрачный миксин будет выглядеть так:
Этим кодом мы создаём функцию
Широко известно, что в старых версиях IE у свойства
В if-ie.styl свойство
Если мы по умолчанию не отдаём обычным браузерам
С помощью Stylus это делается очень просто, при этом можно сэкономить довольно много байт: ведь что нам точно в старых IE не нужно, так это свойства с префиксами.
Выше в статье я уже упомянул, что мы используем библиотеку nib — эта библиотека определяет прозрачные миксины для большинства новых CSS-cвойств, например, для транзишнов. В итоге в стилях для Stylus мы пишем просто
Делается это примерно так:
Тут всё почти понятно: одним условием мы только для IE переопределяем сразу все нужные свойства. Однако из-за особенностей Stylus приходится в определении функции написать хотя бы одно правило —
В итоге IE вместо определённого в nib миксина
Не будем раздувать статью и описывать всё, что есть в if-ie.styl — в документации к проекту это уже подробно описано.
Однако нужно рассказать ещё про одну функцию, которая оказалась нам очень полезна в рамках тематизации Яндекс.Почты. Это функция
Что же она делает? Старые IE не поддерживают значения цвета, заданные в формате rgba. Поэтому обычно разработчики либо прописывают соответствующие цвета дважды — сначала для старых IE в обычном hex-формате, а потом уже всем нормальным браузерам в желаемом
Функция
Простой пример:
В обычных браузерах этот цвет будет обычным
Более сложный пример, с целевым фоном в качестве последнего аргумента (и с использованием одной из фич Stylus — возможности указать вместо трёх цифр
В этом примере для нормальных браузеров будет цвет
При этом есть возможность задать и фолбек по умолчанию с помощью переменной
В итоге можно очень сильно упростить себе жизнь, если использовать функцию
Это только первая статья из серии статей об использовании препроцессора 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 в этом случае можно будет почти не вспоминать.