Привет, Хабр! Представляю вашему вниманию перевод статьи «Introducing Sass Modules» автора Miriam Suzanne.

Недавно в Sass появилась функциональность, которая вам знакома по другим языкам: модульная система. Это большой шаг вперед для @import, одной из наиболее часто используемых функций в Sass. Несмотря на то, что существующая директива @import позволяет вам подключать сторонние пакеты и разделять ваши стили на поддерживаемые элементы, у неё всё же есть несколько ограничений:

  • @import также есть и в CSS, и какие-либо различия в их поведении могут сбивать с толку.
  • Если вы делаете @import несколько раз для одного файла, то это может замедлить компиляцию, вызвать конфликты переопределения и на выходе вы получите дублированный код.
  • Все находится в глобальной области видимости, включая сторонние пакеты — так моя функция color может переопределить вашу существующую функцию color или наоборот.
  • Когда вы используете функцию, например, color, невозможно точно узнать, где она определена. Какой @import подключил ее?

Авторы Sass-пакетов (как и я) пытались обойти проблемы с пространством имен, вручную расставляя префиксы для переменных и функций — но Sass модули гораздо более мощное решение. Вкратце, @import заменяется более явными правилами @use и @forward. В течение следующих нескольких лет @import в Sass будет считаться устаревшим, а затем будет удален. Вы по-прежнему можете использовать CSS Import, но они не будут компилироваться Sass'ом. Но не волнуйтесь, существует инструмент для миграции, который поможет вам обновиться.

Импортирование файлов с помощью @use


@use 'buttons';

Новый @use похож на @import, но у него есть некоторые заметные различия:

  • Файл импортируется единожды, неважно сколько раз вы используете @use в проекте.
  • Переменные, миксины и функции (которые в Sass называются «членами»), начинающиеся с подчеркивания (_) или дефиса (-), считаются приватными и не импортируются.
  • Члены из подключенного через @use файла (в наше случае buttons.scss) доступны только локально и не передаются последующему импорту.
  • Аналогично, @extends будет применяться только вверх по цепочке; то есть расширение применяется только к стилям, которые импортируются, а не к стилям, которые импортируют.
  • Все импортированные члены по умолчанию имеют свое пространство имен.

Когда мы подключаем файл через @use, Sass автоматически генерирует пространство имен на основе имени файла.

@use 'buttons'; /* создает пространство имен `buttons`*/
@use 'forms'; /* создает пространство имен `forms`*/

Теперь у нас есть доступ к членам как файла buttons.scss, так и файла forms.scss, но этот доступ не передаётся между импортами: forms.scss по-прежнему не имеет доступа к переменным, определенным в buttons.scss. Поскольку импортированные сущности имеют пространство имен, мы должны использовать новый синтаксис с разделителем точкой для доступа к ним:

/* переменные: <namespace>.$variable */
$btn-color: buttons.$color;
$form-border: forms.$input-border;

/* функции: <namespace>.function() */
$btn-background: buttons.background();
$form-border: forms.border();

/* миксины: @include <namespace>.mixin() */
@include buttons.submit();
@include forms.input();

Мы можем изменить или удалить пространство имен по умолчанию, добавив к импорту as <name>.

@use 'buttons' as *; /* звездочка удаляет любое пространство имен */
@use 'forms' as 'f';

$btn-color: $color; /* buttons.$color без пространства имен */
$form-border: f.$input-border; /* forms.$input-border пользовательским пространством имен */

Использование as * добавляет модуль в корневое пространство имен, поэтому префикс не нужен, но его члены по-прежнему локально ограничены текущим документом.

Импорт встроенных в Sass модулей


Внутренние возможности в Sass также были перемещены в модульную систему, поэтому мы имеем полный контроль над глобальным пространством имен. Существует несколько встроенных модулей — math, color, string, list, map, selector и meta — которые должны быть импортированы в файл явно перед использованием.

@use 'sass:math';

$half: math.percentage(1/2);

Встроенные модули также могут быть импортированы в глобальное пространство:

@use 'sass:math' as *;

$half: percentage(1/2);

Встроенные функции, которые уже имеют префиксные имена, такие как map-get или str-index, могут использоваться без дублирования этого префикса:

@use 'sass:map';
@use 'sass:string';

$map-get: map.get(('key': 'value'), 'key');
$str-index: string.index('string', 'i');

Вы можете найти полный список встроенных модулей, функций и изменений названий в спецификации модулей Sass.

Новые и измененные основные функции


В качестве дополнительного преимущества это означает, что Sass может безопасно добавлять новые внутренние миксины и функции, не вызывая конфликтов имен. Самый потрясающий пример это миксин load-css из модуля sass:meta. Он работает по аналогии с @use, но только возвращает сгенерированный CSS и работает динамически в любом месте вашего кода:

@use 'sass:meta';
$theme-name: 'dark';

[data-theme='#{$theme-name}'] {
  @include meta.load-css($theme-name);
}

Первый аргумент это URL модуля (как и в @use), но он может быть изменен динамически с помощью переменной, даже с использованием интерполяции, например theme-#{$name}. Второй (необязательный) аргумент принимает структуру map с конфигурацией:

/* Задайте переменную $base-color в 'theme/dark' перед загрузкой */
@include meta.load-css(
  'theme/dark', 
  $with: ('base-color': rebeccapurple)
);

Аргумент $with позволяет сконфигурировать с помощью структуры map любую переменную в загруженном модуле, при этом эта переменная должна удовлетворять условиям:

  • Не является приватной переменной, которая начинается с _ или -
  • Помечена директивой!default

/* theme/_dark.scss */
$base-color: black !default; /* доступна для конфигурации */
$_private: true !default; /* не доступна для конфигурации в силу приватности */
$config: false; /* не доступна для конфигурации, так как не помечена как !default */

Обратите внимание, что ключ 'base-color' устанавливает переменную $base-color.

Есть еще пара новых функций из модуля sass:meta: module-variables() и module-functions(). Каждая их них возвращает структуру map из имён и значений из уже импортированного модуля. Они принимают один аргумент, соответствующий пространству имен модуля:

@use 'forms';

$form-vars: module-variables('forms');
/*
 (
   button-color: blue,
   input-border: thin,
 )
*/

$form-functions: module-functions('forms');
/*
 (
   background: get-function('background'),
   border: get-function('border'),
 )
*/

Несколько других функций из sass:metaglobal-variable-exists(), function-exists(), mixin-exists(), и get-function() — получат дополнительные аргументы $module, которые позволят нам явно проверять каждое пространство имен.

Настройка и масштабирование цветов


У модуля sass:color также есть несколько интересных оговорок по поводу решения некоторых наших старых проблем. Многие из таких устаревших функций, как lighten() или adjust-hue() больше не рекомендуются к использованию в пользу явных функций color.adjust() и color.scale():

/* ранее lighten(red, 20%) */
$light-red: color.adjust(red, $lightness: 20%);

/* ранее adjust-hue(red, 180deg) */
$complement: color.adjust(red, $hue: 180deg);


Некоторые из таких устаревших функций (например, adjust-hue) являются избыточными и ненужными. Другие — такие как lighten, darken, saturate и т.д. — нуждаются в повторной реализации для улучшения внутренней логики. Оригинальные функции были основаны на adjust(), которая использует линейную математику: добавление 20% к текущей светлоте цвета red в нашем примере выше. В большинстве случаев, мы хотим изменять (scale()) цвет на определенный процент относительно текущего значения:

/* теперь прибавляем к светлоте не просто число 20, а число 0.2, умноженное на текущюю светлоту */
$light-red: color.scale(red, $lightness: 20%);

После полного устаревания и удаления эти функции в конечном итоге снова появятся в sass:color с новым поведением, основанным на color.scale(), а не color.adjust(). Это будет происходить постепенно, чтобы избежать внезапных нарушений обратной совместимости. Тем временем я рекомендую вручную проверить ваш код, чтобы увидеть, где color.scale() может оказаться полезнее.

Настройка импортируемых библиотек


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

/* _buttons.scss */
$color: blue !default;

/* old.scss */
$color: red;
@import 'buttons';

Поскольку при использовании модулей больше нет доступа к локальным переменным, нам нужен новый способ задать значения. Мы можем сделать это, передав настройки через map в @use:

@use 'buttons' with (
  $color: red,
  $style: 'flat',
);

Это похоже на аргумент $with в load-css(), но вместо того, чтобы использовать имена переменных в качестве ключей, мы используем сами переменные с символом $.

Мне нравится то, какой явной стала настройка, но есть одно правило, которое сбило меня с толку несколько раз: модуль может быть настроен только один раз при первом использовании. Порядок подключения всегда был важен для Sass, даже с @import, но эти проблемы оставались незамеченными. Теперь мы получаем явную ошибку, и это одновременно хорошо и немного неожиданно. Убедитесь, что подключаете библиотеки через @use и настраиваете их в файле-точке входа (центральный документ, который импортирует все остальные файлы), чтобы эти настройки компилировались перед другими подключениями библиотек через @use.

Невозможно (в данный момент) «связать» конфигурации вместе, сохраняя их редактируемыми, но вы можете обернуть настроенный модуль и передать его как новый модуль.

Передача файлов с помощью @forward


Нам не всегда нужно использовать файл и обращаться к его членам. Иногда мы просто хотим передать его последующему импорту. Допустим, у нас есть несколько файлов, связанных с формами, и мы хотим подключить их все вместе как одно пространство имён. Мы можем сделать это с помощью @forward:

/* forms/_index.scss */
@forward 'input';
@forward 'textarea';
@forward 'select';
@forward 'buttons';

Члены таких проброшенных файлов не доступны в текущем документе и не создаётся никакого пространства имён, но эти переменные, функции и миксины будут доступны, когда другой файл подключит их через @use или пробросит всю коллекцию через @forward. Если переданные отдельные файлы содержат фактический CSS, он также будет передаваться без непосредственной его генерации до тех пор, пока не будет использован сам пакет. На этом этапе все это будет рассматриваться как один модуль с одним пространством имен:

/* styles.scss */
@use 'forms'; /* подключение всех проброшенных членов в пространство имён `forms` */

Для заметки: Если вы попросите Sass подключить папку, то он будет искать в ней файл index или _index.

По умолчанию все публичные члены будут пробрасываться вместе с модулем. Но мы можем быть более избирательными c помощью условий show и hide и указания конкретных членов, которые мы хотим добавить или исключить.

/* пробросить только миксин `border()` и переменную `$border-color` из модуля `input` */
@forward 'input' show border, $border-color;

/* пробросить все члены модуля `buttons` за исключением функции `gradient()` */
@forward 'buttons' hide gradient;

Для заметки: когда функции и миксины имеют общее название, они добавляются и скрываются также вместе.

Для уточнения источников или избежания конфликтов имён проброшенных модулей мы можем добавить префиксы к членам подключенного файла c помощью as:

/* forms/_index.scss */
/* @forward "<url>" as <prefix>-*; */
/* предполагается, что у обоих модулей есть миксин`background()` */
@forward 'input' as input-*;
@forward 'buttons' as btn-*;

/* style.scss */
@use 'forms';
@include forms.input-background();
@include forms.btn-background();

И, если нам нужно, мы всегда можем использовать через @use и пробросить через @forward один и тот же модуль, добавив оба правила:

@forward 'forms';
@use 'forms';

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

/* _tools.scss */
/* библиотека используется только один раз с настройкой */
@use 'accoutrement/sass/tools' with (
  $font-path: '../fonts/',
);
/* пробрасываем настроенную библиотеку */
@forward 'accoutrement/sass/tools';

/* какие-то ещё дополнения... */


/* _anywhere-else.scss */
/* импорт настроенной и расширенной библиотеки */
@use 'tools';

И @use, и @forward должны быть объявлены в корне документа (не вложенном) и в начале файла. Только @charset и простые определения переменных могут появляться перед директивами импорта.

Переход к модульной системе


Чтобы протестировать новый синтаксис, я создала новую Sass библиотеку с открытым исходным кодом (Cascading Color Systems) и новый сайт для моей группы — оба еще в стадии разработки. Мне нужно было понять модули с точки зрения автора библиотеки и с точки зрения разработчика сайта. Давайте начнем с опыта «конечного пользователя» в написании стилей сайта с использованием синтаксиса модулей…

Поддержка и написание стилей


Использование модулей на сайте было приятным. Новый синтаксис поддерживает архитектуру кода, которую я уже использую. Все мои импорты глобальных настроек и инструментов находятся в одной директории (я называю её config) с индексным файлом, который передаёт все, что мне нужно:

/* config/_index.scss */
@forward 'tools';
@forward 'fonts';
@forward 'scale';
@forward 'colors';

Разрабатывая другие части сайта, я могу импортировать эти инструменты и конфигурации везде, где они мне нужны:

/* layout/_banner.scss */
@use '../config';

.page-title {
  @include config.font-family('header');
}

Это даже работает вместе с моими существующими библиотеками, такими как Accoutrement и Herman, которые до сих пор используют старый синтаксис @import. Так как правило @import не будет заменено везде одним разом, разработчики Sass дали некоторое время для перехода. Модули доступны уже сейчас, но @import не устареет еще год или два — и будет удален из языка только через год после этого. В то же время, две системы будут работать вместе любым способом:

  • Если мы выполним @import для файла, который содержит новый синтаксис @use/@forward, то только публичные члены будут импортированы без пространства имён.
  • Если мы выполним @use или @forward для файла, который содержит старый синтаксис @import, мы получаем доступ ко всем вложенным импортам в виде единого пространства имен.

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

Инструмент миграции


Обновление не займет много времени, если мы будем использовать инструмент миграции, созданный Jennifer Thakar. Он может быть установлен с помощью NPM, Chocolatey или Homebrew:

npm install -g sass-migrator
choco install sass-migrator
brew install sass/sass/migrator

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

Мигратор может быть запущен из командной строки и, надеюсь, будет добавлен в сторонние приложения, такие как CodeKit и Scout. Указываете ему на один файл Sass, например style.scss и говорите ему, какие миграции применить. На данный момент существует только одна миграция под названием module:

# sass-migrator <migration> <entrypoint.scss...>
sass-migrator module style.scss

По умолчанию мигратор обновляет только один файл, но в большинстве случаев мы хотим обновить основной файл и все его зависимости: любые элементы, подключенные через @import, @forward или @use. Мы можем это сделать, указав каждый файл по отдельности или просто добавив флаг --migrate-deps.

sass-migrator --migrate-deps module style.scss

Для пробного запуска мы можем добавить --dry-run --verbose (или в сокращенной форме -nv) и посмотреть на результаты без изменения исходных файлов. Существует ряд других опций, которые мы можем использовать для настройки миграции — даже есть одна, специально предназначенная для помощи авторам библиотек в удалении старых пространств имен, созданных вручную — но я не буду описывать все из них здесь. Инструмент миграции полностью документирован на веб-сайте Sass.

Обновление опубликованных библиотек


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

Важная вещь, которую нужно знать прямо сейчас, заключается в том, что Sass обеспечил нам защиту в течение переходного периода. Мало того, что старый импорт и модули могут работать вместе, мы можем создавать файлы «import-only», чтобы обеспечить более удобную работу для пользователей, которые по-прежнему подключают наши библиотеки через @import. В большинстве случаев это будет альтернативная версия основного файла пакета, и вы захотите, чтобы они были рядом: <name>.scss для пользователей модулей и <name>.import.scss для старых пользователей. Каждый раз, когда пользователь вызывает @import <name>, он загружает .import-версию файла:

/* загружает `_forms.scss` */
@use 'forms';

/* загружает `_forms.import.scss` */
@import 'forms';


Это особенно полезно для добавления префиксов для разработчиков, которые не используют модули:

/* _forms.import.scss */
/* Передача основного модуля с добавлением префикса */
@forward 'forms' as forms-*;


Обновление Sass


Возможно, вы помните, что Sass несколько лет назад замораживал добавление новых функций, чтобы различные его реализации (LibSass, Node Sass, Dart Sass) догнали оригинальную реализацию на Ruby, чтобы в итоге полностью отказаться от неё. Заморозка завершилась в прошлом году с несколькими новыми функциями и активными обсуждениями и разработкой на GitHub — но не так торжественно. Если вы пропустили эти релизы, то вы можете почитать блог Sass:


В настоящее время Dart Sass является канонической реализацией и, как правило, первым внедряет новые функции. Я рекомендую переключиться на него, если хотите получать всё самое последнее. Вы можете установить Dart Sass с помощью NPM, Chocolatey или Homebrew. Он также отлично работает c gulp-sass.

Подобно CSS (начиная с CSS3), больше нет единого номера версии для новых выпусков. Все реализации Sass работают с одинаковой спецификацией, но у каждой из них есть уникальный график выпуска и нумерация, что отражено в информации о поддержке в новой красивой документации, дизайнером которой выступила Jina.

image

Модули Sass доступны с 1 октября 2019 года в Dart Sass 1.23.0.

Комментарии (0)