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

Пол­го­да на­зад я стал мейн­тей­не­ром (О том, как это про­изо­шло и чем имен­но я там за­ни­ма­юсь, я на­пи­шу как-ни­будь в дру­гой раз, тут сто­ит за­ме­тить толь­ко то, что я яв­ля­юсь имен­но мейн­тей­не­ром, а ос­нов­ным раз­ра­бот­чи­ком сей­час яв­ля­ет­ся мой кол­ле­га Миша Ко­ре­па­нов) Стай­лу­са —от­лич­но­го пре­про­цес­со­ра для CSS.

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

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

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

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

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

По­сле того как мы пе­ре­да­ли блок в мик­син, этот блок стал до­сту­пен внут­ри мик­си­на как име­но­ван­ный ар­гу­мент —block. По­сле чего его мож­но вы­ве­сти (Та­к­же мы мо­жем пе­ре­дать эту пе­ре­мен­ную в дру­гой мик­син, или вос­поль­зо­вать­ся ей как-то ина­че, при­мер в кон­це ста­тьи за­вя­зан имен­но на этом) че­рез ин­тер­по­ля­цию (В бу­ду­щем, воз­мож­но, до­ба­вит­ся воз­мож­ность ис­поль­зо­вать его без ин­тер­по­ля­ции):

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
  }
}

Как вид­но из при­ме­ра, син­так­сис по­хож на обыч­ные явас­крип­то­вые объ­ек­ты: клю­чом мо­жет быть или иден­ти­фи­ка­тор, или стро­ка, а зна­че­ни­ем мо­жет быть по­чти что угод­но, в том чис­ле и вло­жен­ный хеш. Из важ­но­го: в от­ли­чие от обыч­ных бло­ков Стай­лу­са, фи­гур­ные скоб­ки для хе­шей обя­за­тель­ны, но вот за­пя­тые —нет (Как и осталь­ные оп­ци­о­наль­ные фичи син­так­си­са, если не при­дер­жи­вать­ся ка­ко­го-то кон­крет­но­го код-стай­ла, то код бу­дет нечи­та­бель­ным, так что ре­ши­те как вы бу­де­те пи­сать хеши и бло­ки, и при­дер­жи­вай­тесь это­го ре­ше­ния).

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

foo.bar = 20px
foo['what­ever'] = 'hello'

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

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

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

Функ­ция se­lec­tor()

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

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

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

if match(':(be­fore|af­ter)', se­lec­tor())
  con­tent: ''

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

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

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

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

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

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

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

$me­di­a_­cache = {}

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

me­dia($con­di­tion)
  un­less $me­di­a_­cache[$con­di­tion]
    $me­di­a_­cache[$con­di­tion] = ()
  push($me­di­a_­cache[$con­di­tion], block)

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

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

+me­dia('(max-width:640px)')
  .foo
    dis­play: block;

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

.foo
  +me­dia('(max-width:640px)')
    dis­play: block;

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

me­dia($con­di­tion)
  helper($con­di­tion)
    un­less $me­di­a_­cache[$con­di­tion]
      $me­di­a_­cache[$con­di­tion] = ()
    push($me­di­a_­cache[$con­di­tion], block)

  +helper($con­di­tion)
    {se­lec­tor()}
      {block}

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

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

ap­ply_­me­di­a_­cache()
  for $me­dia, $blocks in $me­di­a_­cache
    @me­dia $me­dia
      for $block in $blocks
        {$block}

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

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

ap­ply_­me­di­a_­cache()

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

Од­на­ко, и эту функ­цию мож­но улуч­шить: ведь мы не хо­тим каж­дый раз пи­сать при вы­зо­ве ско­боч­ки, да и, на са­мом деле, хо­ро­шо бы все­гда иметь в усло­ви­ях only screen and. Кро­ме того, мы и во­все мо­жем за­хо­теть ис­поль­зо­вать вме­сто кон­крет­ных зна­че­ний клю­че­вые сло­ва, типа palm, portable, desk (Для при­ме­ра я вы­брал гра­да­ции из фрейм­вор­ка Гар­ри Ро­берт­са inuit.css) и т.п. Вме­сте с до­пол­не­ни­я­ми и все­ми преды­ду­щи­ми ша­га­ми мы по­лу­ча­ем вот та­кой код:

Ито­го­вый код

// Опре­де­ля­ем объ­ект кеша и объ­ект с али­а­са­ми
$me­di­a_­cache = {}
$me­di­a_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)'
}

// Мик­син, ке­ши­ру­ю­щий ме­ди­а­к­ве­ри
me­dia($con­di­tion)
  helper($con­di­tion)
    un­less $me­di­a_­cache[$con­di­tion]
      $me­di­a_­cache[$con­di­tion] = ()
    push($me­di­a_­cache[$con­di­tion], block)

  +helper($con­di­tion)
    {se­lec­tor() + ''}
      {block}

// Функ­ция, вы­зы­ва­ю­щая за­ке­ши­ро­ван­ные ме­ди­а­к­ве­ри
ap­ply_­me­di­a_­cache()
  for $me­dia, $blocks in $me­di­a_­cache
    $me­dia = un­quote($me­di­a_aliases[$me­dia] || $me­dia)
    $me­dia = '(%s)' % $me­dia un­less match('\(', $me­dia)
    $me­dia = 'only screen and %s' % $me­dia
    @me­dia $me­dia
      for $block in $blocks
        {$block}

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

// Вы­зы­ва­ем все за­ке­ши­ро­ван­ные ме­ди­а­к­ве­ри
ap­ply_­me­di­a_­cache()

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

.foo
  width: 10px

  +me­dia('lap')
    width: 20px

  +me­dia('desk')
    width: 30px

  +me­dia('min-width: 200px')
    width: 60px

.bar
  height: 10px

  +me­dia('lap')
    height: 20px

  +me­dia('desk')
    height: 30px

  +me­dia('min-width: 200px')
    height: 50px

  +me­dia('(min-width: 500px) and (max-width: 700px)')
    height: 50px

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

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

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

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

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