Статьи

Когда и как поддерживать несколько версий Sass

На днях я рассматривал код Sass системы сетки Джит , просто ради этого. После некоторых комментариев к репозиторию GitHub я понял, что сопровождающие Jeet еще не готовы перейти на Sass 3.3. На самом деле, точнее будет сказать, что пользователи Jeet не готовы перейти на Sass 3.3, в зависимости от количества проблем, открытых, когда Jeet начал использовать функции Sass 3.3.

В любом случае, дело в том, что Джит не может получить все классные и блестящие вещи из Sass 3.3 . Или это может?

Если вам известно, какая версия 3.3 представлена ​​в Sass , вы можете знать, что в ядро ​​была добавлена ​​пара вспомогательных функций, призванных помочь разработчикам фреймворка одновременно поддерживать несколько версий Sass:

  • global-variable-exists($name) : проверяет, существует ли переменная в глобальной области видимости
  • variable-exists($name) : проверяет, существует ли переменная в текущей области видимости
  • function-exists($name) : проверяет, существует ли функция в глобальной области видимости
  • mixin-exists($name) : проверяет, существует ли миксин в глобальной области видимости

Есть также feature-exists($name) , но я действительно не уверен, что она делает, так как документы довольно уклончивы. Я даже взглянул на код функции , но он не делает ничего, кроме bool(Sass.has_feature?(feature.value)) , который мало помогает.

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

Хорошо, новые функции, довольно круто. Но что происходит, когда мы используем одну из этих функций в среде Sass 3.2.x? Давайте выясним это с небольшим примером.

1
2
3
4
5
6
7
// Defining a variable
$my-awesome-variable: 42;
 
// Somewhere else in the code
$does-my-awesome-variable-exist: variable-exists(‘my-awesome-variable’);
// Sass 3.3 -> `true`
// Sass 3.2 -> `variable-exists(‘my-awesome-variable’)`

Как видно из результатов, Sass 3.2 не вылетает и не выдает никаких ошибок. Он анализирует variable-exists('my-awesome-variable') как строку, поэтому в основном "variable-exists('my-awesome-variable')" . Чтобы проверить, имеем ли мы дело с логическим значением или строкой, мы можем написать очень простой тест:

1
2
3
$return-type: type-of($does-my-awesome-variable-exist);
// Sass 3.3 -> `bool`
// Sass 3.2 -> `string`

Теперь мы можем определить версию Sass из кода. Насколько это круто? На самом деле, мы точно не определяем версию Sass; скорее мы находим способ определить, используем ли мы Sass 3.2 или Sass 3.3, но это все, что нам нужно в этом случае.

Давайте посмотрим на привнесение прогрессивного улучшения в функции Sass. Например, мы могли бы использовать нативные инструменты, если они доступны (Sass 3.3), или использовать пользовательские инструменты, если они не доступны (Sass 3.2). Это то, что я предложил Джету в отношении функции replace-nth() , которая используется для замены значения по определенному индексу.

Вот как мы могли это сделать:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@function replace-nth($list, $index, $value) {
  // If `set-nth` does exist (Sass 3.3)
  @if function-exists(‘set-nth’) == true {
    @return set-nth($list, $index, $value);
  }
 
  // Else it’s Sass 3.2
  $result: ();
  $index: if($index < 0, length($list) + $index + 1, $index);
 
  @for $i from 1 through length($list) {
    $result: append($result, if($i == $index, $value, nth($list, $i)));
  }
 
  @return $result;
}

И потом, я полагаю, вы похожи … какой смысл делать это, если мы все равно сможем заставить его работать на Sass 3.2? Честный вопрос Я бы сказал производительность . В нашем случае set-nth является нативной функцией из Sass 3.3, что означает, что она работает в Ruby, что означает, что она намного быстрее, чем пользовательская функция Sass. По сути, манипуляции выполняются на стороне Ruby, а не на компиляторе Sass.

Другим примером (все еще из Jeet) была бы reverse функция, переворачивающая список значений. Когда я впервые выпустил SassyLists , Sass 3.3 не было, поэтому изменение списка означало бы создание нового списка, повторение цикла в обратном направлении по первоначальному списку и добавление значений к новому. Это хорошо сработало. Однако теперь, когда у нас есть доступ к функции set-nth из Sass 3.3, существует гораздо лучший способ перевернуть список: поменять местами индексы.

Чтобы сравнить производительность между обеими реализациями, я попытался поменять местами латинский алфавит (список из 26 элементов) 500 раз. Результаты были более или менее:

  • от 2 до 3 с «подходом 3.2» (с использованием append )
  • никогда не выше 2 с «подходом 3.3» (используя set-nth )

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@function reverse($list) {
  // If `set-nth` does exist (Sass 3.3)
  @if function-exists(‘set-nth’) == true {
    @for $i from 1 through floor(length($list) / 2) {
      $list: set-nth(set-nth($list, $i, nth($list, -$i)), -$i, nth($list, $i));
    }
 
    @return $list;
  }
 
  // Else it’s Sass 3.2
  $result: ();
 
  @for $i from length($list) * -1 through -1 {
    $result: append($result, nth($list, abs($i)));
  }
 
  @return $result;
}

Здесь мы снова используем Sass 3.3, поддерживая Sass 3.2. Это довольно опрятно, тебе не кажется? Конечно, мы могли бы написать функцию наоборот, в первую очередь работая с Sass 3.2. Это не имеет абсолютно никакого значения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@function reverse($list) {
  // If `set-nth` doesn’t exist (Sass 3.2)
  @if function-exists(‘set-nth’) != true {
    $result: ();
 
    @for $i from length($list) * -1 through -1 {
      $result: append($result, nth($list, abs($i)));
    }
 
    @return $result;
  }
 
  // Else it’s Sass 3.3
  @for $i from 1 through floor(length($list) / 2) {
    $list: set-nth(set-nth($list, $i, nth($list, -$i)), -$i, nth($list, $i));
  }
 
  @return $list;
}

Примечание : чтобы проверить, запускаем ли мы Sass 3.2 в последнем примере, мы могли бы также проверить function-exists("set-nth") == unquote('function-exists("set-nth")') , но это довольно длинный и подверженный ошибкам.

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

1
$sass-version: if(function-exists(«function-exists») == true, 3.3, 3.2);

Я дам вам что-то хитрое. Позвольте мне объяснить, что здесь происходит. Мы используем троичную функцию if() , сконструированную так:

  • первый аргумент функции if() является условием; оценивается как true или false
  • если условие оценивается как true , оно возвращает второй аргумент
  • иначе он возвращает третий аргумент

Примечание : Sass 3.2 имеет вид глючной троичной функции. Он оценивает все три значения, а не только возвращаемое. Иногда это может привести к неожиданным ошибкам.

Теперь давайте посмотрим, что происходит с Sass 3.3:

  • function-exists('function-exists') возвращает true потому что, очевидно, function-exists() существует
  • тогда function-exists('function-exists') == true — это как true == true — это true
  • поэтому $sass-version установлена ​​на 3.3

И если мы запускаем Sass 3.2:

  • function-exists('function-exists') — это не функция, а строка, поэтому в основном "function-exists('function-exists')"
  • function-exists('function-exists') == truefalse
  • поэтому $sass-version установлена ​​на 3.2

Если вы функциональный человек, вы можете обернуть это в функцию.

1
2
3
@function sass-version() {
  @return if(function-exists(«function-exists») == true, 3.3, 3.2);
}

Тогда используйте это так:

01
02
03
04
05
06
07
08
09
10
11
@if sass-version() == 3.3 {
  // Sass 3.3
}
 
@if sass-version() == 3.2 {
  // Sass 3.2
}
 
@if sass-version() < 3.3 {
  // Sass 3.2
}

Конечно, мы могли бы проверить существование другой функции 3.3, такой как call() или map-get() но потенциально могла бы быть версия Sass, где *-exists функции *-exists , но не call() или карты, поэтому я чувствую, что лучше проверить наличие функции *-exists . И так как мы используем function-exists , давайте проверим это!

Sass 3.3 является первой версией, которая реализует функции *-exists , поэтому мы должны проверить, действительно ли *-exists($param) возвращает логическое значение или анализируется как строка, что является своего рода хакерством.

Теперь предположим, что Sass 3.4 будет выпущен завтра с функцией unicorn() , приносящей удивительность и радугу миру. Функция для определения версии Sass, вероятно, будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
@function sass-version() {
  @if function-exists(‘unicorn’) == true {
    @return 3.4;
  }
  @else if function-exists(‘unicorn’) == false {
    @return 3.3;
  }
  @else {
    @return 3.2;
  }
}
Неаполитанский единорог Эрин Хантинг
Неаполитанский единорог Эрин Хантинг

И затем, если в Sass 3.5 появилась функция rainbow() , вы должны обновить sass-version() следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@function sass-version() {
  @if function-exists(‘rainbow’) == true {
    @return 3.5;
  }
  @else if function-exists(‘unicorn’) == true
       and function-exists(‘rainbow’) == false {
    @return 3.4;
  }
  @else if function-exists(‘unicorn’) == false {
    @return 3.3;
  }
  @else {
    @return 3.2;
  }
}

И так далее.

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

В любом случае, представьте, что мы можем импортировать файл на основе результата функции sass-version() . Это сделало бы чертовски простым использование функций Sass 3.3 для Sass 3.2.

Например, мы могли бы иметь файл, содержащий все версии картографических функций Sass 3.2, используя вместо этого двумерные списки (как это делал Лу Нельсон с Sass-List-Maps ), и импортировать его только при работе с Sass 3.2, например так:

1
2
3
4
// Unfortunately, this doesn’t work 🙁
@if sass-version() < 3.3 {
  @import «polyfills/maps»;
}

Затем мы могли бы использовать все эти функции (например, map-get ) в нашем коде, не беспокоясь о версии Sass. Sass 3.3 будет использовать нативные функции, в то время как Sass 3.2 будет использовать полифилы.

Но это не работает.

Можно прийти с идеей определения функций в условном выражении вместо импорта целого файла. Тогда мы можем определить функции, связанные с картой, только если они еще не существуют (другими словами: Sass 3.2). К сожалению, это тоже не работает: функции и миксины не могут быть определены в директиве.

Функции не могут быть определены в управляющих директивах или других миксинах.

Лучшее, что мы можем сделать на данный момент, — это определить версию Sass 3.2 и Sass 3.3 в каждой функции, как мы видели в начале этой статьи. Но он не только более сложен в обслуживании, но также требует, чтобы каждая нативная функция Sass 3.3 была обернута в пользовательскую функцию. Взгляните назад на нашу функцию replace-nth представленную ранее: мы не можем назвать ее set-nth() , или она будет бесконечно рекурсивной при использовании Sass 3.3. Поэтому мы должны найти произвольное имя (в данном случае replace-nth ).

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

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

1
2
3
@if sass-version() < 3.3 {
  @warn «You are using a version of Sass prior to 3.3. Unfortunately for you, Sass 3.3 is required for this tool to work. Please make sure to upgrade your Sass compiler.»;
}

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

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

На мой взгляд, изучение того, как определить версию Sass и использовать функцию *-exists станет важным, по крайней мере, для некоторых проектов (фреймворки, грид-системы, библиотеки …). А пока продолжайте Sassing, ребята!