овые фичи в Стайлусе

Полгода назад я стал мейнтейнеромGo to a sidenote Стайлуса — отличного препроцессора для CSS.

Side note: О том, как это произошло и чем именно я там занимаюсь, я напишу как-нибудь в другой раз, тут стоит заметить только то, что я являюсь именно мейнтейнером, а основным разработчиком сейчас является мой коллега Миша Корепанов. Jump to this sidenote’s context.

Side note: Можете сразу промотать до его пошагового описания, либо до результирующего кода. Jump to this sidenote’s context.

На прошлой неделе мы зарелизили новую версию — 0.41.0, в которой добавили пару важных фич. А в двух релизах до этого мы добавили поддержку хешей и отполировали её, в результате этих трёх последних релизов теперь можно делать много всего интересного. В этой заметке я опишу один подходGo to a sidenote, который теперь можно применять в Стайлусе, но для начала я напишу немного про новые фичи.

лочные миксины

Наконец-то! То, чего так давно не хватало в Стайлусе (и что уже давно есть в Sass) — возможность передавать в миксин блок кода.

Синтаксис передачи блока довольно простой: вызываем миксин, используя префикс «+», после чего передаём соответствующий блок либо в фигурных скобках, либо через блок с новым отступом (как всё обычно делается в Стайлусе):

+foo()
    // Блок, который мы хотим передать
    width: 10px
    height: 10px

После того как мы передали блок в миксин, этот блок стал доступен внутри миксина как именованный аргумент — block. После чего его можно вывестиGo to a sidenote через интерполяциюGo to a sidenote:

Side note: Также мы можем передать эту переменную в другой миксин, или воспользоваться ей как-то иначе, пример в конце статьи завязан именно на этом Jump to this sidenote’s context.

Side note: В будущем, возможно, добавится возможность использовать его без интерполяции Jump to this sidenote’s context.

foo()
  width: 20px
  .foo
    {block}

Если вызвать этот миксин, например, так:

.bar
  +foo()
    padding: 0
    .baz
      height: 20px

Мы получим следующее:

.bar {
  width: 20px;
}
.bar .foo {
  padding: 0;
}
.bar .foo .baz {
  height: 20px;
}

Мы получили возможность обрамлять блоки с помощью миксинов во что угодно (а в будущем, возможно, добавим и возможность изменять переданные блоки). Обычно это используют для работы с медиакверями, — мой пример, который будет ниже в статье, как раз из той же области.

еши

Хеши — объекты вида «свойство-значение». Выглядят они довольно просто:

foo = {
  bar: 10px,
  raz: #fff,
  baz: {
    blah: blah
    '10%': yeah
  }
}

Как видно из примера, синтаксис похож на обычные яваскриптовые объекты: ключом может быть или идентификатор, или строка, а значением может быть почти что угодно, в том числе и вложенный хеш. Из важного: в отличие от обычных блоков Стайлуса, фигурные скобки для хешей обязательны, но вот запятые — нетGo to a sidenote.

После того, как вы объявили хеш, можно в него добавить новые свойства или перезаписать старые либо через точку, либо через квадратные скобки:

foo.bar = 20px
foo['whatever'] = 'hello'

Отличия довольно простые: через точку можно писать только идентификаторы, тогда как в квадратных скобках можно использовать любые строки, либо передавать переменные. В общем, с квадратными скобками получается более гибко, а через точку — короче.

Получать свойства можно аналогично — либо через точку, либо через квадратные скобки.

Не буду описывать остальные возможности хешей — их довольно много, отмечу, что с ними нормально работает встроенная функция Стайлуса length(), по ним можно итерироваться, можно проверять наличие ключей в условиях (if baz in foo), а также есть несколько встроенных функций для работы с хешами (keys(), values(), merge()) и интерполяция хешей в CSS-код.

ункция selector()

В новом Стайлусе появилась небольшая, но важная фича — функция selector(). До неё в Стайлусе не было возможности получить текущий селектор: его можно было составлять из вложенных блоков, интерполировать в него, но узнать какой же в итоге получается селектор было нельзя.

Теперь же есть функция selector(), которая возвращает текущий скомпилированный селектор. Его можно использовать либо для различных проверок, либо для каких-либо иных целей. Уже сейчас эта функция будет очень полезна в разных ситуациях, а в будущих релизах она станет ещё мощнее.

В качестве примера я приведу вот такой небольшой кусок кода:

if match(':(before|after)', selector())
  content: ''

Здесь мы проверяем есть ли в селекторе указание на псевдоэлемент, и если так — выводим content. Это может пригодиться, если у вас есть миксин, целиком отвечающий за какое-то поведение, и который можно применить как к обычному элементу, так и к псевдо-элементу.

ример с кешируемыми медиакверями

В качестве примера использования новых фич я приведу решение одной из проблем модного нынче отзывчивого дизайна: огромного количества переопределений, которые нужно расставлять для разных вьюпортов. Проблема заключается в том, что синтаксис вызова медиакверей довольно развесистый, поэтому приходится либо не обращать на это внимание и использовать «всплытие медиакверей»Go to a sidenote, либо, в погоне за оптимизацией, писать все переопределения рядом, что во многих ситуациях будет менее удобно.

Однако, с блочными миксинами, хешами и функцией selector() в Стайлусе теперь можно обойти эти проблемы (и попутно решить ещё пару других).

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

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

Для начала нам понадобится объект, в который мы будем сохранять вызванный в будущем код:

$media_cache = {}

После этого нам будет нужен миксин, который мы и будем использовать вместо медиакверей, в первом приближении он будет выглядеть как-то так:

media($condition)
  unless $media_cache[$condition]
    $media_cache[$condition] = ()
  push($media_cache[$condition], block)

Миксин довольно простой: если у нас ещё нет в кеше списка по переданному в миксин ключу, мы инициируем этот список, после чего пушим в него переданный в миксин блок — это будет наш кеш.

На самом деле нам этого не будет достаточно: такой миксин можно будет использовать только вот так:

+media('(max-width:640px)')
  .foo
    display: block;

Мы сможем прокидывать внутрь только полноценные блоки, у нас не получится использовать всплытие:

.foo
  +media('(max-width:640px)')
    display: block;

Всё из-за того, что миксин пока ничего не знает о своём контексте — он знает только о блоке, который в него передали. Тут-то нам и поможет функция selector(), да ещё один миксин-помощник — вместе с ними наш миксин будет выглядеть так:

media($condition)
  helper($condition)
    unless $media_cache[$condition]
      $media_cache[$condition] = ()
    push($media_cache[$condition], block)

  +helper($condition)
    {selector()}
      {block}

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

Так как теперь при вызове нашего миксина всё будет помещаться в кеш, оно не будет выводиться само по себе. Значит, нам нужна функция, которая возьмёт содержимое кеша и выплеснет его там, где мы эту функцию применим (логично будет вызывать её в конце файла):

apply_media_cache()
  for $media, $blocks in $media_cache
    @media $media
      for $block in $blocks
        {$block}

Всё довольно просто: сначала итерируемся по кешу, получая по очереди условия ($media) и список вызванных с таким условием блоков ($blocks), после чего создаём соответствующую медиаквери, внутри которой уже итерируемся по всем блокам, выводя их один за другим.

Теперь, если мы в конце документа вызовем эту функцию:

apply_media_cache()

мы получим то, ради чего всё затевали.

Однако, и эту функцию можно улучшить: ведь мы не хотим каждый раз писать при вызове скобочки, да и, на самом деле, хорошо бы всегда иметь в условиях only screen and . Кроме того, мы и вовсе можем захотеть использовать вместо конкретных значений ключевые словаGo to a sidenote, типа palm, portable, desk и т.п. Вместе с дополнениями и всеми предыдущими шагами мы получаем вот такой код:

тоговый код

    // Определяем объект кеша и объект с алиасами
    $media_cache = {}
    $media_aliases = {
      palm:       '(max-width: 480px)'
      lap:        '(min-width: 481px) and (max-width: 1023px)'
      lap-and-up: '(min-width: 481px)'
      portable:   '(max-width: 1023px)'
      desk:       '(min-width: 1024px)'
      desk-wide:  '(min-width: 1200px)'
    }

    // Миксин, кеширующий медиаквери
    media($condition)
      helper($condition)
        unless $media_cache[$condition]
          $media_cache[$condition] = ()
        push($media_cache[$condition], block)

      +helper($condition)
        {selector() + ''}
          {block}

    // Функция, вызывающая закешированные медиаквери
    apply_media_cache()
      for $media, $blocks in $media_cache
        $media = unquote($media_aliases[$media] || $media)
        $media = '(%s)' % $media unless match('\(', $media)
        $media = 'only screen and %s' % $media
        @media $media
          for $block in $blocks
            {$block}

    // Здесь будет основной код с вызовами миксина
    // …

    // Вызываем все закешированные медиаквери
    apply_media_cache()

Теперь мы можем писать код примерно так:

    .foo
      width: 10px

      +media('lap')
        width: 20px

      +media('desk')
        width: 30px

      +media('min-width: 200px')
        width: 60px

    .bar
      height: 10px

      +media('lap')
        height: 20px

      +media('desk')
        height: 30px

      +media('min-width: 200px')
        height: 50px

      +media('(min-width: 500px) and (max-width: 700px)')
        height: 50px

И в результате получим следующее:

    .foo {
      width: 10px;
    }
    .bar {
      height: 10px;
    }
    @media only screen and (min-width: 481px) and (max-width: 1023px) {
      .foo {
        width: 20px;
      }
      .bar {
        height: 20px;
      }
    }
    @media only screen and (min-width: 1024px) {
      .foo {
        width: 30px;
      }
      .bar {
        height: 30px;
      }
    }
    @media only screen and (min-width: 200px) {
      .foo {
        width: 60px;
      }
      .bar {
        height: 50px;
      }
    }
    @media only screen and (min-width: 500px) and (max-width: 700px) {
      .bar {
        height: 50px;
      }
    }

В последнем варианте функции apply_media_cache можно увидеть, что мы добавили объект с алиасами. Кроме того, мы теперь можем вызывать миксин как в сокращённом варианте, без скобок, так и со скобками — все варианты будут работать.

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

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


Вы можете прокомментировать эту статью в Мастодоне.