Статьи

Sass Mixin и медиа слияния

Если вы не очень хорошо знакомы с Sass, вы можете не знать, что Sass расширяет медиазапросы (фактически, директиву @media ) для предоставления дополнительных интересных функций. Один из которых называется (или, скорее, часто упоминается) слияние медиа .

Прежде чем объяснить вам, что такое слияние медиафайлов, знали ли вы, что спецификации CSS в медиа-запросах действительно позволяют вкладывать медиа-запросы? Некоторые браузеры, такие как Firefox, Chrome и Opera поддерживают это; некоторые другие, такие как Safari и Internet Explorer, в настоящее время нет.

Поскольку поддержка браузера не идеальна, Sass запускает и объединяет вложенные медиазапросы в одно условие медиа. Например, рассмотрим этот код:

 @media (min-width: 42em) { @media (max-width: 1337px) { .foo { color: red; } } } 

Это будет скомпилировано как:

 @media (min-width: 42em) and (max-width: 1337px) { .foo { color: red; } } 

Довольно удобно, не правда ли? Это то, что называется слиянием СМИ . Когда вложенные медиа-запросы объединяются в один оператор.

Что мы хотим? Медиа-запросы!

Теперь, когда я закончил введение, позвольте мне перейти к сути. На днях я играл с этой идеей. По сути, я хотел создать очень простой миксин, который принимает карту запросов в качестве входных данных и объединяет их в одно условие в директиве @media в качестве выходных данных.

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

 @mixin media($queries) { .. } .foo { @include media((min-width: 42em, max-width: 1337px)) { color: red; } } 

И при компиляции, получая тот же результат, что и в фрагменте CSS выше. Теперь, на макушке головы, я могу придумать как минимум два способа его построения. Давайте сначала займемся уродливым.

Уродливая версия

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

 /// Media query merger (the ugly version) /// Create a single media condition out of a map of queries /// @param {Map} $queries - Map of media queries @mixin media($queries) { $media-condition: '; // Loop over the key/value pairs in $queries @each $key, $value in $queries { // Create the current media query $media-query: '(' + $key + ': ' + $value + ')'; // Append it to the media condition $media-condition: $media-condition + $media-query; // If pair is not the last in $queries, add a `and` keyword @if index(map-keys($queries), $key) != length($queries) { $media-condition: $media-condition + ' and '; } } // Output the content in the media condition @media #{$media} { @content; } } 

Ладно, это не так страшно. Это хорошо справляется с работой, но вы должны признать, что это не очень элегантно, не так ли?

Элегантный способ

Мне не очень удобно манипулировать строками, когда Sass предоставляет такой элегантный способ обработки медиазапросов. Конечно, есть лучший способ сделать это. А потом меня поразило : рекурсия . Согласно бесплатному словарю , рекурсия это:

Метод определения последовательности объектов, таких как выражение, функция или набор, где задано некоторое количество начальных объектов и каждый последующий объект определяется в терминах предшествующих объектов. Последовательность Фибоначчи определяется рекурсией.

Это немного сложно. Если говорить очень просто, рекурсия — это механизм, где функция вызывает себя снова и снова с разными аргументами до определенной точки. Практический пример функции, использующей рекурсию в JavaScript:

 function factorial(num) { if (num < 0) return -1; else if (num == 0) return 1; else return (num * factorial(num - 1)); } 

Как видите, функция вызывает себя до тех пор, пока переменная num станет меньше 1 , уменьшая ее на 1 при каждом запуске.

Почему я говорю тебе это? Я понял, что мы могли бы использовать рекурсию для создания нашего медиа-состояния, используя слияния Sass. Что если мы сделаем, чтобы миксин выводил медиа для первого запроса на карте, а затем вызывал себя, пропуская карту без этого запроса, пока на карте не осталось запросов? Давайте попробуем, и так как это может быть немного сложным, мы пойдем шаг за шагом.

Во-первых, мы теперь, что если наша карта не содержит (больше) запроса, мы просто выводим содержимое. Что мы не начнем с этого?

 @mixin media($queries) { @if length($queries) == 0 { @content; } @else { // ... } } 

Теперь мы хотим вывести медиа-блок для первого медиа-запроса на карте. Чтобы получить первый ключ карты, мы можем использовать функции nth(..) и map-keys(..) .

 $first-key: nth(map-keys($queries), 1); @media ($first-key: map-get($queries, $first-key)) { // ... } 

Все идет нормально. Теперь нам нужно только сделать сам вызов mixin, хотя мы не хотим передавать ему ту же карту $queries иначе мы столкнемся с бесконечным циклом. Нам нужно передать $queries после удаления первой пары ключ / значение. К счастью, для этого есть функция map-remove(..) .

 $queries: map-remove($queries, $first-key); @include media($queries) { @content; } 

Теперь весь миксин:

 /// Media query merger /// Create a single media condition out of a map of queries /// @param {Map} $queries - Map of media queries @mixin media($queries) { @if length($queries) == 0 { @content; } @else { $first-key: nth(map-keys($queries), 1); @media ($first-key: map-get($queries, $first-key)) { $queries: map-remove($queries, $first-key); @include media($queries) { @content; } } } } 

Идти дальше

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

 /// Breakpoints map /// @type Map $breakpoints: ( 'small': (min-width: 767px), 'medium': (min-width: 992px), 'large': (min-width: 1200px), ); /// Responsive breakpoint manager /// @param {String} $breakpoint - Breakpoint /// @requires $breakpoints @mixin respond-to($breakpoint) { $media: map-get($breakpoints, $breakpoint); @if not $media { @error "No query could be retrieved from `#{$breakpoint}`. " + "Please make sure it is defined in `$breakpoints` map."; } @media #{inspect($media)} { @content; } } 

Этот миксин работает как талисман, но он не поддерживает условия множественных запросов, такие как (min-width: 42em) and (max-width: 1337px) потому что он опирается на функцию inspect(..) которая выполняет только печать Нахальное представление стоимости.

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

Или это?

Немного подправив миксин respond-to(..) , мы можем включить в него миксин media(..) вместо печати самой директивы @media . Тогда у нас есть лучшее из обоих миров.

 @mixin respond-to($breakpoint) { // Get the query map for $breakpoints map $queries: map-get($breakpoints, $breakpoint); // If there is no query called $breakpoint in map, throw an error @if not $queries { @error "No value could be retrieved from `#{$breakpoint}`. " + "Please make sure it is defined in `$breakpoints` map."; } // Include the media mixin with $queries @include media($queries) { @content; } } 

Лучше всего, если вы уже используете этот миксин, вы можете полностью включить функцию множественных запросов, настроив respond-to(..) и добавив media(..) потому что API не меняется вообще: respond-to(..) все еще нужно имя точки останова для работы, так же, как и раньше.

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

Должен сказать, я нахожу это очень интересным, потому что впервые нашел хороший вариант использования как для вложенных медиазапросов, так и для рекурсии mixin. Хотя это можно пропустить и просто создать строку, как мы видели в нашей первой версии, гораздо проще и интереснее справиться с ней с помощью рекурсивного миксина. Надеюсь, вам понравилось! Последний пример перед отъездом:

 // _variables.scss $breakpoints: ( 'small': (min-width: 767px), 'small-portrait': (min-width: 767px, orientation: portrait), 'medium': (min-width: 992px), 'large': (min-width: 1200px), ); // _mixins.scss @mixin media($queries) { .. } @mixin respond-to($breakpoint) { .. } // _component.scss .foo { @include respond-to('small-portrait') { color: red; } } 

Получив следующий CSS:

 @media (min-width: 767px) and (orientation: portrait) { .foo { color: red; } }