Почему мы стали использовать препроцессор Stylus в Яндекс.Почте, а также о библиотеке помогающей жить с IE

в 11:14, , рубрики: css, less, sass, stylus, Блог компании Яндекс, Веб-разработка, препроцессоры, Яндекс.Почта, метки: , , , , ,

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

Отличий, в сравнении с вариантом без дополнительных функций, два:

  1. В обоих файлах нужно подключать библиотеку до всех остальных стилей, и обязательно оба раза. (Может показаться, что хватило бы только первого — ведь таблица стилей для IE уже содержит основную таблицу стилей — но тогда некоторые последующие возможности не будут работать). В этом файле уже определяется ie ?= false, и поэтому в основной таблице стилей не нужно это явно прописывать.
  2. В таблице стилей для 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js