Правила управления переменными в препроцессорах и методика переопределения настроек


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


Предыстория


В 2014 году в компании начали редизайн проекта и в основу вёрстки мы положили свежий на тот момент Bootstrap 3.0.1. Использовали мы его не как отдельную стороннюю библиотеку, а тесно заинтегрировали с нашим собственным кодом: отредактировали переменные под наш дизайн и компилировали кастомизированный Бутстрап из LESS исходников самостоятельно. Проект оброс собственными модулями, которые использовали бутстраповские переменные и добавляли в файл с настройками свои новые переменные.


В тот момент я думал, что это правильный подход.


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


Через год с небольшим редизайн закончился, проект вышел в продакшн, и мы взялись за технический долг. При попытке обновить Бутстрап до версии 3.6.x выяснилось, что смержить новый variables.less с нашим файлом настроек будет нелегко. В Бутстрапе переименовали или убрали часть переменных, добавили новые. Собственные компоненты Бутстрапа обновились без проблем, а вот наши компоненты падали при компиляции из-за этих изменений.


Проблемы


Мы проанализировали ситуацию и сформулировали проблемы.


  1. Слишком связанный код.


    Сами компоненты лежали в отдельных файлах. Это создавало иллюзию, что компоненты независимые. Но компоненты использовали переменные, объявленные во внешнем файле variables.less, и без этого файла не работали. Нельзя было просто так взять и переместить компонент в другой проект. Приходилось тянуть за собой файл с настройками, который со временем превратился в помойку.


  2. Слишком много глобальных переменных.


    У Бутстрапа было ?400 переменных. Мы отключили неиспользуемые компоненты Бутстрапа, но переменные оставили в конфиге на случай, если снова понадобятся. Еще мы добавили сотню или полторы своих переменных. Все названия не запомнить, трудно быстро находить нужные. Даже с правилами именования и комментариями ориентироваться в конфиге из 500+ переменных тяжело.


  3. Имена глобальных переменных вышли из-под контроля.


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



Как решать


Я придумал три правила, которые помогли побороть наши проблемы:


  1. Переменная используется только в том файле, в котором объявлена.


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


  2. Все переменные внутри компонента — локальные.


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


  3. Глобальные переменные используются только внутри файла настроек.


    Благодаря первым двум правилам мы сильно сократим количество глобальных переменных, но они всё равно нужны. Глобальные переменные объявляются в главном файле проекта или в файле типа config.less. К ним тоже применяется правило №1 — переменные не используются за пределами своего файла. Это значит, что нельзя использовать глобальные переменные внутри файлов компонентов. Но существует способ не нарушая первого правила прокинуть значение глобальной переменной внутрь компонента. Как это сделать мы рассмотрим на примерах далее.



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


Применим правила на практике.


Реализация на LESS


Представим простейший компонент для стилизации страницы. Согласно правилу №1 создадим переменные внутри файла компонента.


Правило 1
Переменная используется только в том файле, в котором объявлена.

/* page.css */

.page {
    padding: 40px;
    color: #000;
    background-color: #fff;
}

Было. Пример кода компонента.


// page.less v0.1

@padding: 40px;
@txt-color: #000;
@bg-color: #fff;

.page {
    padding: @padding;
    color: @txt-color;
    background-color: @bg-color;
}

Стало. Переменные объявлены в глобальной области видимости и у них слишком общие имена. Это плохо.


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


Локальные переменные


Область видимости — это «пространство» между фигурными скобками селектора: { и }. Объявленные внутри фигурных скобок переменные работают только внутри этих скобок и внутри дочерних скобок, но их нельзя использовать снаружи.


Если скобок вокруг нет, то это самый верхний уровень — глобальная область видимости.


У переменной из дочерней области видимости приоритет выше, чем у одноименной переменной из родительской области видимости. У глобальных переменных самый низкий приоритет при совпадении имён.


По правилу №2 сделаем переменные локальными — переместим их внутрь селектора.


Правило 2
Все переменные внутри компонента — локальные.

// page.less v0.2

.page {
    @padding: 40px;
    @txt-color: #000;
    @bg-color: #fff;

    padding: @padding;
    color: @txt-color;
    background-color: @bg-color;
}

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


Теперь конфликта имен с другими компонентами не будет. Мы решили первую и вторую проблемы: получился изолированный сниппет без внешних зависимостей. Но я не знаю способа переопределить локальные переменные компонента в таком виде извне. Поэтому, чтобы кастомизировать компоненты глобальными переменными проекта, придумал другую форму записи.


Миксины как функции


В LESS можно использовать миксины как функции. Если внутри миксина создать переменные, а потом вызвать миксин внутри селектора, то переменные будут доступны в области видимости этого селектора.


Читайте про миксины как функции в документации LESS.

Вынесем объявление переменных внутрь миксина .page-settings(), а потом вызовем его внутри селектора .page:


// page.less v0.3

.page-settings() {
    @padding: 40px;
    @txt-color: #000;
    @bg-color: #fff;
}

.page {
    .page-settings();

    padding: @padding;
    color: @txt-color;
    background-color: @bg-color;
}

Миксин доставляет переменные в зону видимости селектора.


Переменные локализованы внутри глобального миксина. Когда мы вызвали миксин в коде, переменные стали доступны в области видимости селектора .page, но по прежнему остались локальными.


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


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


Слияние миксинов


В LESS существует «ленивое вычисление» переменных: не обязательно объявлять LESS-переменную перед её использованием, можно объявить после. В момент компиляции парсер LESS найдет определение переменной и отрендерит значение этой переменной в CSS.


Смотрите примеры «Lazy Evaluation» и переопределения дефолтных переменных в документации LESS.

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


Итак, переменные можно объявлять и до, и после использования, а миксины?—?это разновидность переменных. Если создать два миксина с одним и тем же именем и разным содержанием, то они объединяют свои внутренности. А если внутри миксинов есть переменные с одинаковыми именами, то происходит переопределение. Приоритет выше у последнего миксина.


Рассмотрим три файла:


// projectname.less

@import 'normalize.css';
@import 'typography.less';
@import 'page.less';
// и много других компонентов...

@import 'config.less';

Главный файл. Импортируем компоненты и конфиг. Конфиг?—?последним.


// page.less v0.3

.page-settings() {
    @padding: 40px;
    @txt-color: #000;
    @bg-color: #fff;
}

.page {
    .page-settings();

    padding: @padding;
    color: @txt-color;
    background-color: @bg-color;
}

Компонент. Все переменные локальные и хранятся в миксине.


// config.less

@glob-text-color: white;
@glob-bg-color: darkblue;

// кастомизация компонентов
.page-settings() {
    @txt-color: @glob-text-color;
    @bg-color: @glob-bg-color;
}

Конфиг проекта. Переопределяем параметры компонента с помощью миксина настроек.


Всё самое интересное происходит в конфиге. Мы создали глобальные переменные и использовали их в этом же файле для кастомизации компонента.


Миксин .page-settings() объявлен два раза. Первый раз внутри файла page.less с дефолтными значениями, второй раз в файле config.less с новыми значениями. На этапе компиляции миксины склеиваются, новые переменные переопределяют дефолтные и CSS рендерится с новыми значениями из файла конфигурации.


Правило 3
Глобальные переменные используются только внутри файла настроек.

Обратите внимание, что config.less инклюдится последним в списке. Это нужно, чтобы объявление миксина в конфиге имело больший приоритет, чем исходное объявление в файле самого компонента. Настройки не применятся, если поставить config.less до компонента, потому что на миксины тоже действуют правило «последнее определение?—?самое сильное».


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


  1. переменные использовались только в своём файле, даже глобальные переменные не вызывались за пределами файла config.less;
  2. переменные компонента остались локальными и не засорили глобальную область видимости;
  3. глобальные переменные не использовались внутри компонента напрямую, но значения глобальных переменных хитрым способом попали внутрь компонента.

Ограничения


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


// так нельзя

@txt-color: white;

.page-settings() {
    // тут рекурсия и ошибка компиляции
    @txt-color: @txt-color;
}

Неправильно. Рекурсивное определение переменной вызывает ошибку компиляции.


// правильно — с префиксом

@glob-txt-color: white;

.page-settings() {
    // всё в порядке
    @txt-color: @glob-txt-color;
}

Правильно. У глобальных переменных свой префикс glob-, что исключает совпадение имён.


Реализация в SASS


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


Наборы переменных для настройки компонента удобно хранить в map-объектах. Это массив из пар «ключ: значение». Метод map-get извлекает конкретное значение из массива, метод map-mergeобъединяет два массива в один, дополняя или переписывая исходный массив.


Читайте про Maps в документации SASS.

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


// page.scss v0.1

$page-settings: (
    padding: 40px,
    bg-color: #fff,
    text-color: #000,
);

.page {
    padding:          map-get($page-settings, padding);
    background-color: map-get($page-settings, bg-color);
    color:            map-get($page-settings, text-color);
}

Настройки хранятся в map-объекте и вызываются в коде с помощью map-get


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


// projectname.scss

@import: 'config';

@import: 'normalize';
@import: 'typography';
@import: 'page';
// и много других компонентов...

Главный файл.
Сначала импортируем конфиг, потом компоненты.


// config.scss

$glob-text-color: white;
$glob-bg-color: darkblue;

// кастомизируем
$page-settings: (
    bg-color: $glob-bg-color,
    text-color: $glob-text-color,
);

Настройки.
Создаём глобальные переменные и переопределяем параметры компонента.


// page.scss v0.2

$page-override: ( ); // [1]

@if variable-exists(page-settings) {
    $page-override: $page-settings; // [2]
}

$page-settings:  map-merge((
    padding: 40px,
    bg-color: #fff,
    text-color: #000,
), $page-override); // [3]

.page {
    padding:          map-get($page-settings, padding);
    background-color: map-get($page-settings, bg-color);
    color:            map-get($page-settings, text-color);
}

Компонент.
Добавили проверку: а не существуют ли уже настройки, чтобы переопределить компонент?


[1]?—?В компоненте мы сначала объявили переменную с пустым массивом $page-override.


[2]?—?Потом проверили, а не существует ли уже переменная $page-settings. И если она уже была объявлена в конфиге, то присвоили её значение переменной $page-override.


[3]?—?И потом смержили исходный конфиг и $page-override в переменную $page-settings.


Если массив $page-settings был объявлен ранее в глобальном конфиге, то $page-override перепишет настройки при слиянии. Иначе в переменной $page-override будет пустой массив, и в настройках останутся исходные значения.


Я не знаю всех тонкостей SASS, может есть способ реализовать эту методику более красивым способом.

В результате, мы получаем всё те же преимущества, только в отличие от LESS нам приходится переопределять все настройки заранее, перед их использованием в коде.


Выводы


Не важно на чем вы пишете?—?на LESS, SASS, CSS c кастомными свойствами или Javascript?—?глобальных переменных должно быть как можно меньше.


С CSS препроцессорами используйте три правила, которые помогут избежать хаоса:


  1. Переменная используется только в том файле, в котором объявлена.
  2. Все переменные внутри компонента?—?локальные.
  3. Глобальные переменные используются только внутри файла настроек.

Чтобы прокинуть глобальные настройки внутрь компонента, собирайте переменные в миксин (LESS) или map-объект (SASS).


Переопределяйте настройки в правильном месте: в LESS?—?после инклюда, в SASS?—?перед инклюдом.


Реальные примеры


Я сформулировал эту методику в декабре 2015 года для LESS и с тех пор применяю её на рабочих и личных проектах.


За полтора года появилось несколько публичных npm-пакетов. Посмотрите исходный код для лучшего понимания, как это работает в реальных ситуациях.


bettertext.css?—?типографика для сайтов. Настраивается при помощи 11 переменных, остальные 40 вычисляются по формулам. Вычисления идут отдельными миксином, чтобы была возможность переопределять формулы. У компонента нет ни одного класса, все стили применяются к тегам. Чтобы создать локальную область видимости, я поместил все селекторы по тегам в переменную?—?в LESS это называется «detached ruleset».


Читайте про detached ruleset в документации LESS.

links.less?—?стили для ссылок с фокусом, анимацией и бледным подчеркиванием. У компонента кроме миксина с настройками есть дополнительные глобальные миксины для раскрашивания ссылок внутри ваших собственных селекторов.


flxgrid.css?—?генератор HTML-сеток на флексах. Настраивается при помощи 5 переменных, генерирует классы для адаптивной сетки с любыми брейкпоинтами и любым количеством колонок. В компоненте вычисления и служебные миксины спрятаны внутрь локальной области видимости. Глобально виден только миксин с настройками.


space.less?—?инструмент для управления отступами в вёрстке. Создан, чтобы работать в паре с сеткой flxgrid.css. Адаптивность у них настраивается одинаково, но space.less использует собственный миксин настройки и собственные локальные переменные?—?в коде space.less никак не связан с flxgrid.css.


«Бонус-трек»


Если бы мне сейчас понадобилось использовать на новом проекте Bootstrap 3.x.x?—?тот, который на LESS,?—?я бы все импортируемые модули Бутстрапа завернул в переменную (в «detached ruleset»), а все настройки из файла variables.less в миксин bootsrtap-settings. Глобальные переменные Бутстрапа перестали бы быть глобальными и их невозможно было бы использовать внутри собственного кода. Настройки Бутстрапа я бы кастомизировал по мере необходимости, вызывая миксин bootsrtap-settings в конфиге проекта, так же как в примерах выше. Тогда при обновлениях Бутстрапа пришлось бы поправить только миксин с кастомизированными настройкам.




Первоисточник?—?http://paulradzkov.com/2017/local_variables/

Поделиться с друзьями
-->

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


  1. greybax
    10.07.2017 21:17

    Первоисточник?—?http://paulradzkov.com/2017/local_variables/

    Эээм… а зачем копипастить свою же статью еще и на хабр?


    1. paulradzkov
      10.07.2017 22:04

      Захотелось поделиться с широкой аудиторией.
      Мне в личку уже напомнили, что копипаста это нарушение правил Хабра.
      Прошу прощения :)


  1. ArmorDarks
    10.07.2017 23:37
    +6

    Спасибо за статью!


    Сначала мне показалось что идея забавная, но потом понял что перемудрили.


    С точки зрения Sass, логичней было использовать флаг !default, это позволило бы избежать использование объектов в целом и связанной с ними проблемы склеивания, а также весьма громоздкий способ извлечения данных из этого самого объекта, и вообще приличного количества boilerplate-кода только для того, чтобы это работало в каждом компоненте.


    Кроме того, с точки зрения оптимизации, в Sass все эти склейки, проверки на присутствие переменной, а также map-get в больших количествах неприятно отразятся на скорости компиляции в больших проектах, и это так даже, увы, с libsass.


    Что касается глобальных переменных, то их было бы достаточно именовать с префиксами, как уже было сказано в статьи, (global или именим фреймворка, как у нас. Это эффективно изолирует их от локальных переменных, в следствии чего в локальных файлах можно смело использовать переменные без каких-либо префиксов и хитрых оберток в виде объекта — они ничего не сломают, потому что с глобальными переменными конфликтовать никогда не будут, а если такая же локальная переменная ($padding) встретится в другом компоненте, это тоже ничего не сломает, при условии что все используемые в данном файле локальные переменные всегда будут объявляться в начале файла:


    // ../global-config.scss
    $global-padding: 10px;

    // ../component-1.scss
    $padding: $global-padding;
    
    .Component-1 {
      padding: $padding;
    }

    // ../component-2.scss
    $padding: 20px;
    
    .Component-2 {
      padding: $padding;
    }

    Результат:


    .Component-1 {
      padding: 10px;
    }
    
    .Component-2 {
      padding: 20px;
    }

    Вот пример как компонент Kotsu устанавливает локальное значение $padding, которое по умолчанию устанавливается на глобальное значение ekzo-space(). Просто и без каких-либо излишеств.


    Если же в проекте нужна истинная конфигурация именно компонента, который предположительно будет экспортировать или многократно использоваться, то с описанной методикой тоже есть какой-то неоправданный overhead. К примеру, если бы упомянутый bettertext.less писался на Sass, то все хитро передаваемые значения по умолчанию было бы куда логичней поместить в аргументы самого миксина:


    image


    Пример такого миксина:


    @mixin Component-1(
      $width: ekzo-space(16),
      $height: ekzo-space(10)
    ) {
    
      // включать сюда название класса — вопрос спорный, но пост не об этом
      .Component-1 {
        width: $width;
        height: $height;
      }
    
    }
    
    @include Component-1();
    @include Component-1($width: 200px);

    Результат:


    .Component-1 {
      width: 384px;
      height: 240px;
    }
    
    .Component-1 {
      width: 200px;
      height: 240px;
    }

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


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


    1. paulradzkov
      11.07.2017 14:22
      -1

      Мне тоже не нравится медленный boilerplate-код в Sass, но !default не работает так, как мне надо. Смотрите, почему.


      Демка https://codepen.io/paulradzkov/pen/WOYygN


      //global config
      $glob-text-color: white;
      $glob-bg-color: darkblue;
      
      $bg-color: $glob-bg-color;
      $text-color: $glob-text-color;
      
      //eof global config
      
      //component.scss
      
      body {
          $bg-color: white !default;
          $text-color: black !default;
          $padding: 40px !default;
          padding: $padding;
          background-color: $bg-color;
          color: $text-color;
      }
      //eof component.scss

      Мы объявляем локальные переменные внутри компонентов с флагом !default. Пока мы не пытаемся их переопределить извне, всё хорошо — переменные локальные, соседним компонентам не мешают.


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


      Локальные переменные становятся глобальными и у нас сразу же возникает необходимость разделять их при помощи префиксов. Разделение по неймспейсу — это имитация инкапсуляции. Такое разделение требует значительных усилий по поддержанию порядка.


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


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


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


      Далее, в вашем примере:


      // ../global-config.scss
      $global-padding: 10px;
      // end of ../global-config.scss
      
      // ../component-1.scss
      $padding: $global-padding;
      
      .Component-1 {
        padding: $padding;
      }
      // end of ../component-1.scss
      
      // ../component-2.scss
      $padding: 20px;
      
      .Component-2 {
        padding: $padding;
      }
      // end of ../component-2.scss

      $padding — объявлена в глобальной области видимости и без префикса. Значит в компоненте №3, №4 и так далее паддинг будет равен 20px. Это уже ошибка.


      В примере «компонент Kotsu» переменная $wrapper-padding не локальная, она объявлена в глобальной области видимости. Кроме этого, для её определения использована переменная из другого файла. Т.е. ваш компонент не сможет скомпилироваться самостоятельно без внешнего файла, в котором объявлена переменная ekzo-space().


      Я стараюсь этого избежать, мне нужно, чтобы компоненты были по-настоящему независимыми.


      Я пробовал вариант с !default maps. Демка https://codepen.io/paulradzkov/pen/xrQzGE


      //global config
      $glob-text-color: white;
      $glob-bg-color: darkblue;
      
      $page-settings:  (
          bg-color: $glob-bg-color,
          text-color: $glob-text-color,
      );
      //eof global config
      
      //component.scss
      $page-settings:  (
          bg-color: white,
          text-color: black,
          padding: 40px
      ) !default;
      
      body {
          padding: map-get($page-settings, padding);
          background-color: map-get($page-settings, bg-color);
          color: map-get($page-settings, text-color);
      }
      //eof component.scss

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


      С переменными как параметрами миксина тоже не то. Чтобы миксин отренедерил в CSS свой код, его обязательно нужно вызвать, с параметрами или без.


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


      1. ArmorDarks
        11.07.2017 16:49
        +1

        Ремарка: буду продолжать говорить исключительно о Sass, поскольку с Less у меня опыт не полноценный и там скоупинг работает иначе.


        Мы объявляем локальные переменные внутри компонентов с флагом !default. Пока мы не пытаемся их переопределить извне, всё хорошо — переменные локальные, соседним компонентам не мешают.

        Здесь проблема в том, что вы пытаетесь создать инкапсуляцию кустарным методом.


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


        Локальные переменные становятся глобальными и у нас сразу же возникает необходимость разделять их при помощи префиксов. Разделение по неймспейсу — это имитация инкапсуляции. Такое разделение требует значительных усилий по поддержанию порядка.

        Как я уже говорил, неймспейсить нужно только глобальные переменные, это нормальная практика для большинства языков программирования. Неймспейсить "локальные"" переменные (даже если они в глобальном скоупе, но используются только локально) — затея, как вы уже сами сказали, болезненная, но она и не нужная. Она ничего не дает. В том же Ruby и Python так не далают. В JavaScript иногда тоже можно встречаться с этой проблемой, но и в этом случае ничего неймспейсить не нужно — последний let myVar перезапишет переменную с нужным нам значением в текущем скоупе.


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

        Если я правильно понял из описание, то вот как раз это "инвертированное" или "всеобщее" прифексирование в Бутраспе и создает эти неприятные запутывания.


        Там где нужны были действительно глобальные переменные — нужно было префексировать. В остальных случаях было логичней инкапсулировать параметры как дефолтные в миксины.


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


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

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


        Посмотрите на глобальный конфиг стилей Kotsu — там нет глобальных переменных, которые можно использовать где-то случайно и потом не понимать, к чему приведет их изменение. Разве что в команде есть проблемы с людьми, которые делают что-то бездумно, но это уже совсем другого рода проблема.


        $padding — объявлена в глобальной области видимости и без префикса. Значит в компоненте №3, №4 и так далее паддинг будет равен 20px. Это уже ошибка.

        Именно так, я это и пытался подчеркнуть в своем посте выше.


        Переменная $padding действительно является глобальной, но используется только локально.


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


        Кроме этого, для её определения использована переменная из другого файла. Т.е. ваш компонент не сможет скомпилироваться самостоятельно без внешнего файла, в котором объявлена переменная ekzo-space().

        Это был пример зависимости от глобальной конфигурации. Ее можно сделать опциональной с помощью


        $wrapper-padding: if(function-exists(ekzo-space), ekzo-space(), 50px);

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


        Значит в компоненте №3, №4 и так далее паддинг будет равен 20px

        Этой проблемы не должно возникать, поскольку если компонент №3 и №4 используют ту же "локально-глобальную" переменную $padding, она должна всегда объявляться в начале файла. Я не могу себе представить компонент, который без объявления переменной вдруг с бухты-барахты начнет использовать неизвестную переменную $padding.


        Нужно понимать, что Sass во многом наследует Ruby, а в нем, как и в Python, скоупинг переменных работает иначе, чем в JavaScript. К примеру, ситуация когда по скоупу выше будет встречаться по своей сути глобальная переменная с весьма общим названием padding = 10, а потом глубже — padding = 20 абсолютно нормальная, в этом нет никакой проблемы. Проблемы могут быть разве что у разработчика, который почему-то забывает объявить нужные переменные, чтобы случайно не использовать глобальное значение.


        Я пробовал вариант с !default maps. Демка https://codepen.io/paulradzkov/pen/xrQzGE

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


        С переменными как параметрами миксина тоже не то. Чтобы миксин отренедерил в CSS свой код, его обязательно нужно вызвать, с параметрами или без.

        В этом нет никаких проблем. Миксин — это по сути своей функция, как раз форма инкапсуляции. И я уже писал выше о том, что во многих языках это нормально вызвать функцию после импорта, и это как раз ожидаемое поведение. Необходимость что-либо импортировать, а потом внезапно вызвать какие-то другие непонятные миксины (а в случае Sass еще и прописывать внезапную переменную $*-override) для конфигурации другого миксина — это это как раза не ожидаемое поведение.


        К слову, стоит отметить, что переменная $*-override никак не заизолирована, и из-за того что она не является обязательной, да еще и автоматически мерджится с дефолтными настройками, шансов случайно закинуть что-либо в нее из другого компонента весьма много.


        В моей системе компонент достаточно заинклюдить, тогда он отрендерит CSS с дефолтными параметрами. Если нужно его настроить, перепределить нужные переменные в конфиге. В сам компонент изменения вносить не нужно.

        Да, но для этого у вас целый свой огород с велосипедами. Если в Less-версии они смотрятся по крайней мере приемлимо, сам язык предрасположен к таким манипуляциям, то по Sass явно видно как он сопротивляется всему этому и результат не просто для конечного восприятия и использования. Это как-то уже намекает на то, что в подходе что-то не так.


        1. paulradzkov
          20.07.2017 01:49

          Я пишу в основном на Less и методику сформулировал изначально для Less. Реализация на Sass — это калька с Less методики. Да, Sass сопротивляется и то, как всё это выглядит на Sass, мне не нравится.


          Я согласен, что в Sass проще избежать проблем, с которыми я встретился в Less. Достаточно обнулять переменные в начале каждого компонента. В Less так не получится. Правило №1 по прежнему в силе и для Less, и для Sass: объявлять переменные в том же файле, в котором они используются.


          Насчет компонентов, которые вызываются параметрическим миксином, удобно ли это, когда параметров много? Например, у bettertext.css 11 параметров для настройки и еще 40 переменных вычисляются из них. В Less благодаря слиянию миксинов я могу переопределять не только основные параметры, но и формулы вычислений. Как сделать такое в Sass не представляю.
          Я за то, чтобы усложнить компонент внутри, но сделать его проще в (пере)использовании.


  1. nweb
    12.07.2017 18:41

    Есть ли профит от использования переменных только внутри компонента? Если компонент небольшой, то больше трудозатрат уйдет на объявление переменных.


    1. paulradzkov
      12.07.2017 19:42

      Зависит от ситуации. Профит есть и для маленьких компонентов, в тех случаях, когда в коде повторяются некоторые значения, когда из одних значений вычисляются другие, когда вы планируете использовать этот компонент в другом проекте и там его надо будет перенастроить, когда вы хотите сделать скины/темы оформления компонента.


  1. Pongo
    19.07.2017 23:46

    Немного переделал ваш вариант для less при помощи изоляции переменных через & { @import "page"; }


    Пример


    1. paulradzkov
      20.07.2017 00:44

      Спасибо за пример!


      & { //код } действительно проще для изоляции, чем вариант с detached ruleset, который использовал я, чтобы создать локальную область видимости на несколько селекторов:


      @page-render: {
      
        .page { ... }
        .page__body { ... }
      
      };
      
      @page-render();

      Я стремился изолировать каждый компонент на уровне less-исходника. Изоляция даёт возможность хранить каждый такой компонент как npm-пакет и подключать их на разных проектах. В вашем примере миксин вызывается внутри компонента, но объявлен в другом файле. Без конфига компонент не может скомпилироваться.


      По этой же причине @import (reference) "config"; из самого компонента выглядит крайне нежелательно. Компонент не должен ничего «спрашивать» про окружение и проект, в котором его используют. Вдруг конфиг называется по другому или его вообще не существует.


      1. Pongo
        20.07.2017 00:54

        Да, не подумал об этом. Я добавил в свой пример файл "page2.less", в котором миксин указан в самом файле, и конфиг не импортируется. Но пришлось использовать двойной скоуп при помощи &{} — странновато выглядит.


        1. paulradzkov
          20.07.2017 02:00

          Если в page2.less сократить скоупы, то вы придете к тому же решению, что использую я.


          .page2-settings() {
            @padding: 40px;
            @txt-color: #000;
            @bg-color: #fff;
          }
          
          // благодаря `&{}` объявленные внутри переменные не выходят наружу
          & {
            .page2-settings(); // вызываем дефолтные настройки
          
            .page2 {
              padding: @padding;
              color: @txt-color;
              background-color: @bg-color;
            }
          
            .page2__body {
              padding: @padding * 1.2;
            }
          }
          

          Только у меня конфиг вызывается последней строкой, чтобы он переопределял нужные миксины. В документации Less так и рекомендуют делать http://lesscss.org/features/#variables-feature-default-variables


          1. Pongo
            20.07.2017 08:07

            Да, действительно. Жаль webstorm не умеет находить переменные, объявленные через mixin as function.