Sass模块

原文:Introducing Sass Modules | CSS-Tricks

Sass刚刚发布了一个新的功能:模块化系统。这对sass中经常使用到的@import功能有很大的提升。现在的@import存在一些局限性:

  • @import同时也是一个CSS功能,容易混淆它们的区别
  • 如果对一个文件@import多次,可能会降低编译速度,引起覆盖冲突,并产生重复的输出
  • 所有都是用全局命名空间,包括第三方库——因此我的color()函数可能会覆盖你的color()函数,反之亦然
  • 当使用比如color()的函数,基本不知道它在哪定义的,从哪@import来的

Sass包作者尝试用通过手动添加前缀的方法解决命名空间的问题——但是Sass模块系统提供了更好的解决方案。简而言之,@import正在被有更加明确意义的@use@forward规则取代。未来几年@import将会被弃用,然后删除。你仍然能使用CSS imports,但它们不会被Sass编译。不必担心,这里还有迁移工具来帮助你升级。

使用@use引入文件

@use 'buttons';

新的@use@import很像,但是还有一些明显的区别之处:

  • 无论在项目中使用多少次@use,这个文件只引入一次
  • 以一个下划线(_)或者中划线(-)开头的变量、混合、函数(在Sass中被称为”members”)会被人为是私有的而不会引入
  • 被使用文件中的成员(本例中是buttons.scss)只在本地可用,而不会传递给之后引入的文件
  • 同样,@extends只适用于上一层,只扩展被引入文件中的选择器,而不是引入它的当前文件
  • 所有引入的成员都有默认的命名空间

当我们使用@use来引入文件时,Sass会创建一个基于文件名称的命名空间

@use 'buttons'; // 创建一个 buttons 命名空间
@use 'forms'; // 创建一个 forms 命名空间

现在我们可以使用buttons.scssforms.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 使用定义的命名空间名称

使用*将模块添加到根命名空间,因此不需要前缀名称可以直接使用,但是这些成员依旧只作用于当前文档。

引入内建的Sass模块

Sass的内部属性也被移动到了模块系统中,因此我们可以使用全局命名空间来控制它们。这些内置模块——mathcolorstringlistmapselectormeta——需要在使用前引入。

@use 'sass:math';
$half: math.percentage(1/2);

Sass模块也可以使用全局命名空间

@use 'sass:math' as *;
$half: percentage(1/2);

内建函数名中带有前缀的,比如map-getstr-index,在使用时可以不必重复写前缀

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

你可以在这里查看完整的内建模块、函数和名称变换列表sass/module-system.md at master · sass/sass · GitHub

新增和改变的核心属性

模块化的一个附加好处是,可以安全的添加新的内置函数,而不必考虑命名冲突问题。最让人兴奋的一个例子是sass:meta的混合load-css()。它的作用和@use类似但是只返回创建好的CSS输出,并且可以在我们的代码的任意位置动态使用。

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

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

第一个参数是模块URL(和@use类似),可以使用变量和插值(比如theme-#{$name})来动态的改变。第二个(optional)参数接收可配置的值映射

//  加载前设置 'theme/dark' 中的 $base-color 变量
@include meta.load-css(
  'theme/dark', 
  $with: ('base-color': rebeccapurple)
);

$with参数接收在被加载模块中,满足以下两点的任何变量的键值配置:

  • 没有以_-开头的全局变量(现在用于表示私有)
  • 标记为!default默认值的可配置变量
// theme/_dark.scss
$base-color: black !default; // 可设置的变量
$_private: true !default; // 因为是私有而不可设置
$config: false; // 因为没有 !default 标记而不可设置

base-color将设置$base-color变量

还有两个新增的sass:meta函数:module-variablesmodule-functions()。都接收模块命名空间名称参数,返回已经引入的模块中的成员名称和值。

@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:meta函数——global-variable-exists(), function-exists(), mixin-exists(), 和 get-function()——将接收附加的$module参数,来我们检查每个命名空间

colors的调整和增减

当我们试着摆脱一些遗留问题,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)比较多余,其他的——比如lightendarkensaturate等——需要更好的内部逻辑重建。原始的函数基于adjust(),比如例子中使用线性映射:增加20%red的亮度。在大部分情况下,我们想要基于当前的值scale()亮度的百分比。

// 距离白色 20%, 而不是当前亮度 + 20
$light-red: color.scale(red, $lightness: 20%);

一旦弃用和删除,这些快捷函数会被sass:color代替,sass:color基于color.scale()而不是color.adjust()。为避免突然的向后修改,这些是分阶段进行的。在这期间,推荐您手动查一下代码中哪里使用color.scale()会更好。

配置引用的库

第三方或可复用的库通常会有一些公共可配置变量允许用户修改,我们常常是在引入前定义变量

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

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

使用模块化后不再能修改本地变量,因此我们需要一个新的设置这些默认值的途径:可以添加一个配置给@use

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

这和load-css()里的$with参数很像,不同的是这里直接使用$开头的变量本身,而不是作为keys的变量名。

我非常喜欢这个设置,但是它的一个规则把我绊住了好几次:一个模块只能被设置一次,即在它第一次用到的时候。Sass中的引用顺序往往很重要,即使是使用@import,但是这种问题通常会默默地失效。确保在一个“入口文件”(引入所有文件的文档)中@use和配置库文件,这样就能在其他@use库之前配置。

(目前)不能使用“链式”设置来让模块保持可编辑性,但是可以把已配置的模块和扩展包装在一起,作为新的模块使用。

使用@forward传递文件

我们并不是总要使用一个文件,使用它的成员,有时候只是把它传递给之后要引入它的地方。例如我们有多个表单相关的部分,想把这些部分一起引入为一个命名空间,这时可以使用@forward

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

被传递的文件中的成员在当前文档中不可用,并且没有创建命名空间,但是这些变量、函数和混合可以被其他文件@use或者@forward这个入口集合文件时使用。如果被传递的文件中有CSS,也只是单纯被传递而没有输出,直到它被使用到。到那时,集合文件会作为一个单独的命名空间。

// styles.scss
// 在 forms 命名空间导入所有被传递的 members
@use 'forms'; 

注意:如果让Sass引入一个地址,他会尝试查找目录下名为index或者_index的文件

默认的,所有的公共成员都会被传递,但是也可以使用showhide来选择性是否传递

// 只传递 'input' border() mixin, 和 $border-color 变量
@forward 'input' show border, $border-color;

// 传递所有'buttons' 成员 *除了* gradient() 函数
@forward 'buttons' hide gradient;

注意:如果function和minxin同名,那么这个名字会同时设置两者的显示隐藏

为了让代码清晰或者是避免传递模块之间的命名冲突,可以使用as给成员设置前缀

// forms/_index.scss
// @forward "<url>" 作为 <prefix>-*;
// 假设两个模块都包含一个 background() mixin
@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库(GitHub - mirisuzanne/cascading-color-system: CSS Custom Property color-theming)以及一个我乐队的新网站——它们都尚在建设中。我想要从库作者和网站开发者两种使用角度来理解模块化。让我们以使用模块语法的“最终用户”的体验开始……

维护和书写样式

在网站中使用模块化是一种享受。新语法鼓励的编码风格是我已经在用的。把所有公共设置和工具放到一个单独的目录中(我称之为config),用一个index文件传递所有需要的文件。

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

当我创建网站的其他部分,可以在我需要的地方引入这些配置和工具

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

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

这在我已有的Sass库比如Accoutrement | OddBirdHerman 中,也用@import语法来实现。由于@import并不会在突然之间全部替换,Sass建设了一个过渡期。模块系统现在就可以使用,但是@import在一两年内不会被弃用——并且只会在弃用一年后再从语言中删除。在此期间,两种系统会协同工作:

  • 如果我们@import一个含有新的@use/@forward语法的文件,只有公共成员会被导入,没用命名空间
  • 如果@use@forward一个有老的@import语法的文件,可以使用一个命名空间访问所有嵌套的导入

这就意味着现在就可以开始使用新的模块语法,而不必等到第三方库更新:我也会花一些时间来更新我的那些库

迁移工具

使用Sass: Migrator可以方便的升级。可以用Node,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帮我们处理了一切问题。不仅能让导入(imports)和模块(modules)协同工作,也让我们可以创建“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-*;
Comments
Write a Comment