Comments 13
Первоисточник — http://paulradzkov.com/2017/local_variables/
Эээм… а зачем копипастить свою же статью еще и на хабр?
Спасибо за статью!
Сначала мне показалось что идея забавная, но потом понял что перемудрили.
С точки зрения 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, то все хитро передаваемые значения по умолчанию было бы куда логичней поместить в аргументы самого миксина:
Пример такого миксина:
@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;
}
В итоге, нам не нужно ничего придумать, чтобы как-то передавать, записывать, переопределять и выводить значения, все это уже предусмотренно самим языком.
Но, на практике, такие миксины для многих сайтов являются избыточными, поскольку требует больше времени на декларирование компонента, а также сам вызов миксина немного негативно сказывается на скорости компиляции.
Мне тоже не нравится медленный 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 с дефолтными параметрами. Если нужно его настроить, перепределить нужные переменные в конфиге. В сам компонент изменения вносить не нужно.
Каждый такой компонент независимый и может скомпилироваться самостоятельно. Дополнительное преимущество — локальным переменным не нужны строгие, уникальные имена. В разных компонентах могут быть локальные переменные с одинаковыми именами.
Ремарка: буду продолжать говорить исключительно о 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 явно видно как он сопротивляется всему этому и результат не просто для конечного восприятия и использования. Это как-то уже намекает на то, что в подходе что-то не так.
Я пишу в основном на Less и методику сформулировал изначально для Less. Реализация на Sass — это калька с Less методики. Да, Sass сопротивляется и то, как всё это выглядит на Sass, мне не нравится.
Я согласен, что в Sass проще избежать проблем, с которыми я встретился в Less. Достаточно обнулять переменные в начале каждого компонента. В Less так не получится. Правило №1 по прежнему в силе и для Less, и для Sass: объявлять переменные в том же файле, в котором они используются.
Насчет компонентов, которые вызываются параметрическим миксином, удобно ли это, когда параметров много? Например, у bettertext.css 11 параметров для настройки и еще 40 переменных вычисляются из них. В Less благодаря слиянию миксинов я могу переопределять не только основные параметры, но и формулы вычислений. Как сделать такое в Sass не представляю.
Я за то, чтобы усложнить компонент внутри, но сделать его проще в (пере)использовании.
Зависит от ситуации. Профит есть и для маленьких компонентов, в тех случаях, когда в коде повторяются некоторые значения, когда из одних значений вычисляются другие, когда вы планируете использовать этот компонент в другом проекте и там его надо будет перенастроить, когда вы хотите сделать скины/темы оформления компонента.
Спасибо за пример!
& { //код }
действительно проще для изоляции, чем вариант с detached ruleset, который использовал я, чтобы создать локальную область видимости на несколько селекторов:
@page-render: {
.page { ... }
.page__body { ... }
};
@page-render();
Я стремился изолировать каждый компонент на уровне less-исходника. Изоляция даёт возможность хранить каждый такой компонент как npm-пакет и подключать их на разных проектах. В вашем примере миксин вызывается внутри компонента, но объявлен в другом файле. Без конфига компонент не может скомпилироваться.
По этой же причине @import (reference) "config";
из самого компонента выглядит крайне нежелательно. Компонент не должен ничего «спрашивать» про окружение и проект, в котором его используют. Вдруг конфиг называется по другому или его вообще не существует.
Да, не подумал об этом. Я добавил в свой пример файл "page2.less", в котором миксин указан в самом файле, и конфиг не импортируется. Но пришлось использовать двойной скоуп при помощи &{}
— странновато выглядит.
Если в 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
Как правильно использовать переменные в препроцессорах LESS и SASS