Статьи

Cross-Media Query @extend Директивы в Sass

Если вы читали мою предыдущую статью о директиве Sass @extend или если у вас есть достаточно твердые знания о том, как работает Sass, вы можете знать, что не можете этого сделать:

%example { color: blue; font-weight: bold; font-size: 2em; } @media (max-width: 800px) { .generic-class { @extend %example; } } 

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

Вы не можете @ расширять внешний селектор из @media.
Вы можете использовать только @extend селекторы в пределах одной директивы.

Это отстой, не так ли? Поэтому на днях я засучил рукава и углубился в код, чтобы найти способ создать директиву @extend которая работает с медиа-запросами. Это было удивительное путешествие, и в итоге я нашел не очень страшное решение. Однако помните, что эта техника, по сути, является хакерской, поэтому она в основном экспериментальная . Но если вы не можете дождаться нативного решения Sass, я думаю, вы могли бы попробовать.

Как это работает?

Это очень сложное решение, поэтому вам, возможно, придется повторить это несколько раз. Во-первых, он основан на знании существующих областей @media с самого начала. Как правило, это не должно быть проблемой, потому что наш код обычно имеет список именованных точек останова . Далее, ключ заключается в том, чтобы создать один и тот же заполнитель в каждой области видимости, поэтому независимо от того, в какой области мы в данный момент находитесь, мы можем использовать заполнитель. Затем самое сложное — узнать, в какой области мы сейчас находимся. Например: мы находимся на корневом уровне? Мы в маленькой точке останова? Большой ? Не простая задача.

Поскольку это довольно сложно, результирующий код Sass довольно тяжелый. При этом у меня есть девиз:

Неважно, насколько сложен основной код, если API прост.

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

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

 /* Regular single-scope placeholder */ %my-placeholder { property: value; } /* Enhanced multi-scope placeholder */ @include placeholder(my-placeholder) { property: value; } 

Хотя универсальная версия (2-й пример выше) печатается немного дольше, чем стандартный синтаксис заполнителя, ее так же легко понять. Кроме того, тот факт, что он немного длиннее, не должен иметь большого значения, поскольку вы не создаете заполнители каждые две минуты. Чтобы сократить это, вы можете переименовать его в ph() или даже p() или создать для него псевдонимы.

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

 /* Regular @extend, with cross-scope limitations */ @extend %my-placeholder; /* Enhanced cross-scope @extend */ @include _(my-placeholder); 

Хорошо, теперь я знаю, что это выглядит странно, но перед тем, как вырвать, выслушайте меня: так как нам нужно передать имя заполнителя в качестве параметра миксину, я дал миксину (который вы увидите ниже) очень коротко имя (в данном случае один символ). Это делает его кратким и простым в использовании.

Сначала я хотел использовать %() чтобы соответствовать более распространенному синтаксису заполнителя. Однако для этого потребуется, чтобы символ% был экранирован обратной косой чертой \ (например, \%(my-placeholder) ). Это победило бы цель и сделало бы боль использовать. Отсюда подчеркивание: _() .

Примечание: помните, что Sass считает подчеркивания и дефисы одинаковыми . Так что вместо этого вы можете использовать @include -(my-placeholder) . В противном случае, не стесняйтесь создавать псевдоним, который вы хотите — extend() , gloubiboulga() , god-i-love-sass-so-much() или что-то еще.

В конце концов, я думаю, что API не так уж и плох. Поначалу это может показаться странным, но я думаю, что вы привыкнете к этому довольно быстро, поэтому оно должно быть достаточно хорошим.

Написание библиотеки для нашего API

Давайте подведем итог, что нам нужно для того, чтобы все это работало:

  1. Пара настроек, включая список точек останова
  2. Mixin для обработки медиа-запросов (назовем это breakpoint() )
  3. Mixin для генерации заполнителя ( placeholder() )
  4. Mixin для расширения заполнителя ( _() )

С этого момента вы можете копировать и вставлять каждый из примеров кода в файл Sass в порядке их появления (или в инструмент, подобный SassMeister ), чтобы медленно создавать код, который используется API, обсуждавшимся в предыдущих разделах. Вы также можете просмотреть окончательный код в Gist на SassMeister .

Настройка и настройка

Во-первых, конфигурация. Как я уже сказал, с самого начала нам нужно знать различные точки останова (которые являются нашими областями медиа-запросов). Для этого мы можем использовать простую карту, связывающую ключевое слово со значением max-width . Как это:

 $breakpoints: ( "small" : 600px, "medium" : 900px, "large" : 1200px ); 

Естественно, вы можете выбрать нужные имена и значения ширины. Я решил упростить задачу с 3-мя очень простыми ключевыми словами и 3-мя стандартными значениями ширины для демонстрации, но не стесняйтесь делать как хотите.

Далее нам нужна переменная для хранения текущей точки останова (или области видимости), в которой мы находимся. Давайте назовем ее $current-breakpoint . Нам также нужно дать ему значение по умолчанию, которое может быть любым. Я решил назвать его root , потому что мы начинаем с корневого уровня.

 $default-breakpoint: root; $current-breakpoint: $default-breakpoint; 

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

 $placeholders: (); 

Я думаю, что мы хорошо настроены!

Медиа-запрос Mixin

Миксин медиа-запроса предназначен для открытия новой области . Это означает, что вместо ручного открытия области @media с помощью @media (min-width: 900px) {} мы вместо этого включаем @include breakpoint(small) {} breakpoint , передавая ему ключевое слово (например, @include breakpoint(small) {} ). Конечно, ключевое слово должно быть определено в карте $breakpoint (в этом вся суть). Затем миксин открывает для нас подходящую область.

Интересно, что этот миксин также обновляет переменную $current-breakpoint чтобы отслеживать текущую область видимости. Вот код с комментариями, чтобы прояснить каждый раздел:

 @mixin breakpoint($breakpoint) { // Get the width from the keyword `$breakpoint` // Or `null` if the keyword doesn't exist in `$breakpoints` map $value: map-get($breakpoints, $breakpoint); // If `$breakpoint` exists as a key in `$breakpoints` @if $value != null { // Update `$current-breakpoint` $current-breakpoint: $breakpoint !global; // Open a media query block @media (min-width: $value) { // Let the user dump content @content; } // Then reset `$current-breakpoint` to `$default-breakpoint` (root) $current-breakpoint: $default-breakpoint !global; } // If `$breakpoint` doesn't exist in `$breakpoints`, // Warn the user and do nothing @else { @warn "Invalid breakpoint `#{$breakpoint}`."; } } 

Как вы можете видеть, перед тем, как @content пользовательский контент ( @content ), мы обновляем переменную $current-breakpoint с помощью ключевого слова, переданного в mixin (так, в нашем случае, small , medium или large ), и сразу после его сброса мы сбрасываем его root (т.е. наша переменная $default-breakpoint ).

Примечание: флаг !global был добавлен в Sass 3.3, чтобы сделать различие между определением переменной области действия и переопределением глобальной переменной. В нашем случае мы переопределяем глобальную переменную $current-breakpoint .

Mixin для создания заполнителя

Наш миксин довольно прост. Однако обратите внимание на одну проверку, которую мы проводим перед выполнением каких-либо действий: мы генерируем заполнители только в том случае, если они еще не существуют. Чтобы убедиться, что это не так, мы проверяем имя, переданное в mixin, по списку существующих заполнителей ( $placeholders ). Если его еще нет в списке, мы добавляем его, а затем генерируем заполнители. Это гарантирует, что заполнитель не может быть сгенерирован дважды, что приведет к ошибке.

 @mixin placeholder($name) { // If placeholder doesn't exist yet in `$placeholders` list @if not index($placeholders, $name) { // Store its name $placeholders: append($placeholders, $name) !global; // At root level @at-root { // Looping through `$breakpoints` @each $breakpoint, $value in $breakpoints { // Opening a media query block @media (min-width: $value) { // Generating a placeholder // Called $name-$breakpoint %#{$name}-#{$breakpoint} { @content; } } } // And dumping a placeholder out of any media query as well // so basically at root level %#{$name}-#{$default-breakpoint} { @content; } } } // If placeholder already exists, just warn the user @else { @warn "Placeholder `#{$name}` already exists."; } } 

Расширение заполнителя

Все, что мы оставили, это наш короткий миксин для расширения заполнителя независимо от того, в каком медиа-блоке мы находимся:

 @mixin _($name) { @extend %#{$name}-#{$current-breakpoint} !optional; } 

Именно здесь мы на самом деле используем значение $current-breakpoint , чтобы расширить точный заполнитель из области, в которой мы находимся. !optional флаг !optional Здесь используется только в качестве меры безопасности. Если по какой-либо причине заполнитель не существует (что не должно происходить, но вы никогда не узнаете), @extend не будет аварийно @extend .

Рабочий пример

Давайте создадим простой пример в качестве подтверждения концепции: заполнитель для clearfix. Мы будем простыми; он сбрасывает clear: both для простой очистки поплавка, так и overflow: hidden для очистки внутренней поплавки. Это определенно не самый лучший метод очистки; мы просто используем это для наших целей здесь.

Во-первых, нам нужно создать заполнитель:

 @include placeholder('clear') { clear: both; overflow: hidden; } 

И теперь мы можем использовать это:

 .a { @include _(clear); } .b { @include _(clear); } .c { @include breakpoint(medium) { @include _(clear); } } @include breakpoint(medium) { .d { @include _(clear); } } .e { @include _(clear); @include breakpoint(large) { @include _(clear); } } 

Это приведет к следующему CSS:

 @media (min-width: 900px) { .c, .d { clear: both; overflow: hidden; } } @media (min-width: 1200px) { .e { clear: both; overflow: hidden; } } .a, .b, .e { clear: both; overflow: hidden; } 

Не плохо ли это?

Последние мысли

Таким образом, в итоге нам удалось создать миксины, способные расширять местозаполнитель независимо от того, в каком блоке @media мы находимся, что привело к некоторому аккуратному и оптимизированному выводу CSS. Кроме того, по моему мнению, API не намного сложнее, чем обычный рабочий процесс %placeholder {} / @extend %placeholder .

Теперь это действительно полезно? Если вы спросите меня, я не уверен. Насколько мне известно, мне еще не приходилось сталкиваться с делом, в котором я действительно нуждался в перекрестном охвате @extend . Я думаю, что это случилось со мной только один раз, и мне удалось обойти эту проблему без особых затруднений. Я чувствую, что миксины и заполнители обычно определяют ядро ​​элемента, что означает, что они должны применяться на корневом уровне, а не на определенной точке останова — по крайней мере, так я делаю в своем коде.

Кроме того, использование mixin, когда он застрял в блоке @media без доступа к корневым заполнителям, возможно, намного проще. Кроме того, gzip агрессивно разбивает повторяющиеся строки, поэтому я не уверен, что использование миксинов вместо заполнителей — это плохая идея, когда мы беспокоимся об окончательном размере файла, но это уже другая история.

В любом случае, это забавный эксперимент. Надеюсь, вам понравилось! Кстати, вы можете поиграть с кодом на SassMeister!

Поиграйте с этим в SassMeister.