REXLNT

Sass

CSS Preprocessor

photo by Scott Webb on unsplash.com

Ben Frain 的书中可以看出,他是一个勇于实践、善于学习的开发者。一直以来都很关注他的动态,当他的新书《Sass 和 Compass 设计师指南》初版时,就迫不及待地入手了,这是我和 Sass 的第一次见面。后来机缘巧合翻译了 Sass Guidelines 和其它一些颇具实践性的 Sass 文章,零零散散至今大概有了一年的时间。

预处理器很强大,但它只是编写 CSS 的辅助工具。出于对扩展和维护等方面的考虑,在大型项目中有必要使用预处理器构建 CSS;但是对于小型项目,原生的 CSS 可能是一种更好的选择。不要肆意使用预处理器!

Quick Start

Sass 扩展了 CSS 的现有语法,并提供了一些新的语法糖。在下面的简短代码中,集合了 Sass 中最常用的模块引用 @import、变量和嵌套:

// 设置字符集
@charset "UTF-8";

// 引入模块
@import "reset";

// 创建变量
$primary-color: #333;

// 嵌套
body {
    color: $primary-color;
    ul {
        list-style-type: none;
    }
}

插值字符串

Sass 中的插值字符串 #{$var} 有两方面的作用:动态拼接字符串和去除字符串首尾的引号。示例如下:

@mixin header($tag) {
    #{$tag}:before {
        content: "#{$tag}";
        // 等同于 content: $tag;
    }
}

.post-content {
    @include header("h1");
}

编译结果:

.post-content h1:before {
    content: "h1";
}

父级引用符

可以将父级引用符 & 看做是一个值为父级选择器的插值语法:

a {
    text-decoration: none;
    &:hover {
        text-decoration: underline;
    }
}

编译结果:

a {
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

占位符选择器

占位符选择器是 Sass 特有的一种选择器,声明时以 % 开头,编译时不会输出到 CSS 文件中,主要用于抽象组件的公共部分,配合 @extend 指令实现样式的继承机制:

%font {
    font-size: 14px;
    font-family: "Source Sans Pro";
}

body {
    @extend %font;
}

编译结果:

body {
    font-size: 14px;
    font-family: "Source Sans Pro";
}

数据类型

  • 字符串 (string): "foo", foo
  • 数值 (number): 1.3, 13, 10px
  • 列表 (list): ( 1.5em 1em 0 2em, Helvetica, Arial, sa ns-serif )
  • 映射 (map): ( key1: value1, key2: value2 )
  • 颜色值 (color): blue, #FFFFFF, rgb, hsl, rgba, hsla
  • 布尔值 (bool): true, false
  • 空类型 (null): null

操作符

  • 赋值 :
  • 计算 +-*/%
  • 比较 ==!=>>=<<=
  • 逻辑 andornot

其中,+ 除了用作算术运算之外,也可以用于拼接字符串和求取颜色值。在 Sass 中字符串分为两种:引用字符串(quoted string,外部被引号包裹)和未引用字符串(unquoted string,外部没有引号)。使用 + 拼接字符串时,最终生成的字符串类型为第一个运算子的字符串类型:

body {
    font-family: "Source Sans " + TC;
    p {
        font-family: sans- + "serif";
    }
}

编译结果:

body {
  font-family: "Source Sans TC";
}

body p {
  font-family: sans-serif;
}

使用 + 求取颜色值时,必须保证运算子具有相同的不透明度:

body {
    color: rgba(70, 132, 153, 1) + rgba(32, 68, 121, 1);
    // => color: #66c8ff;

    background-color: rgba(70, 132, 153, .9) + rgba(32, 68, 121, .7);
    // alpha channels must be equal when combining colors 
    // 报错:不透明通道值必须相等
}
`/` 在 CSS 中是有意义的,为了避免和 Sass 除法运算的混淆,所有的除法操作都应该使用小括号 `()` 包裹,比如使用 `font-size: (10px / 2)` 产出 `font-size: 5px`。

变量标识符

Sass 中的变量有三种身份:普通变量、默认值变量(!default)和全局变量(!global),而且这些变量具有作用域的概念,每个代码块 {} 内一个作用域,整个代码文件内也有一个作用域。

当我们引用普通变量时,Sass 首先会从当前作用域开始检索变量,如果找不到就上溯到父级作用域,直到递归到最顶层的作用域:

$color: orange;

div {
    // 对 $color 重新赋值
    $color: blue;
    color: $color;
    p {
        color: $color;
    }
}

a {
    color: $color;
}

编译结果:

div {
  color: blue;
}

div p {
  color: blue;
}

a {
  color: orange;
}

默认值变量往往用于主题的配置文件,起到标识默认值,方便后续的重写覆盖。在最顶层作用域下,变量默认具有全局性,此时使用 !global 并没有实际意义;在块级作用域中,可以通过 !global 将变量提升为全局变量,但这么做势必降低代码的可维护性,所以目前全局变量显得有些鸡肋。

@ 指令

  • @import 模块引用
  • @media 媒体查询
  • @extend 选择器继承
  • @at-root 嵌套提取
  • @debug / @warn / @error 异常和测试

@extend 的强大无可置疑,但是复杂性也一直为人诟病,稍微控制不当就会生成冗余的选择器。归根结底,使用 @extend 是为了继承组件的公有样式,所以在不影响功能的基础上,应该适当的束缚它的能力。到目前为止,最优秀的实践方式就是 @extend 搭配占位符选择器。

%btn {
    color: white;
    font-size: 20px;
}

.btn-danger {
    @extend %btn;
    background-color: red;
}

.btn-default {
    @extend %btn;
    background-color: gray;
}

编译结果:

.btn-danger, .btn-default {
    color: white;
    font-size: 20px;
}

.btn-danger {
    background-color: red;
}

.btn-default {
    background-color: gray;
}

在这个组合中,占位符选择器本身不会被编译到 CSS 文件中,可以节省文件体积,而且 @extend 只继承了单一的占位符选择器,杜绝了选择器泛滥。此外,相比起 @mixin 来,@extend 搭配占位符选择器生成的结果会聚合在同一个样式集中:

@extend vs @mixin

如果再上升一个层次分工的话,那就需要比较一下 @mixin@extend。这两种方式都可以生成公有样式,但是仅此而已就是浪费了 @mixin 的能力。就目前的最佳实践来说,建议使用 @extend 搭配占位符选择器继承公有样式,使用 @mixin 产出动态样式。@mixin 的详细介绍见后续小节。

控制指令

  • @if ... @else if ... @else ... 条件判断
  • @for $var from start through end [startundefined end] 循环
  • @for $var from start to end [startundefined end) 循环
  • @each ... in ... 遍历
  • @while

@each 可以用来遍历 list 和 map 类型的数据,示例如下:

$btn: (
    danger: red, 
    primary: blue, 
    warning: orange
);

@each $type, $color in $btn {
    .btn-#{$type} {
        color: $color;
    }
}

编译结果:

.btn-danger {
    color: red;
}

.btn-primary {
    color: blue;
}

.btn-warning {
    color: orange;
}

混合宏

@extend 部分已经介绍到 @mixin 的一个功能是生成公有样式,但事实上,建议你避开使用该功能,而是着眼于使用 @mixin 动态生成共有样式,更优雅地实现组件复用:

@mixin btn($fontSize, $borderRadius) {
    font-size: $fontSize;
    border-radius: $borderRadius;
}

.btn-sm {
    @include btn(14px, 3px);
}

.btn-lg {
    @include btn(18px, 5px);
}

编译结果:

.btn-sm {
    font-size: 14px;
    border-radius: 3px;
}

.btn-lg {
    font-size: 18px;
    border-radius: 5px;
}

@mixin 的参数除了上面示例的普通参数,还包括默认值参数和不定参数。默认值参数通过提供默认样式,可以在参数缺失时,保障代码的健壮性:

@mixin btn($fontSize, $borderRadius: 5px) {
    font-size: $fontSize;
    border-radius: $borderRadius;
}

.btn-lg {
    @include btn(18px);
}

编译结果:

.btn-lg {
    font-size: 18px;
    border-radius: 5px;
}

不定参数可以保存零个或多个值,最常用的地方就是为同一属性添加多个值,比如多重阴影:

@mixin box-shadow($shadows...) {
    -moz-box-shadow: $shadows;
    -webkit-box-shadow: $shadows;
    box-shadow: $shadows;
}

.shadows {
    @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999);
}

编译结果:

.shadows {
    -moz-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
    -webkit-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
    box-shadow: 0px 4px 5px #666, 2px 6px 10px #999;
}

此外,在传参时也可以使用不定参数:

@mixin colors($text, $background, $border) {
    color: $text;
    background-color: $background;
    border-color: $border;
}

$values: #ff0000, #00ff00, #0000ff;
.primary {
    @include colors($values...);
}

$value-map: (text: #00ff00, background: #0000ff, border: #ff0000);
.secondary {
    @include colors($value-map...);
}

编译结果:

.primary {
    color: #ff0000;
    background-color: #00ff00;
    border-color: #0000ff;
}

.secondary {
    color: #00ff00;
    background-color: #0000ff;
    border-color: #ff0000;
}
在 `@mixin` 中配置参数时,先写普通参数,然后是默认值参数,最后是不定参数。

函数指令 @function

相比起上面的继承和动态生成,@function 在生成方式上自由度更高。此外,还可以嵌套上面的各种指令和操作符,对数据进行筛选、在加工,生成特定样式。在下面的代码中,混合宏根据栅格的数量,动态生成容器的宽度:

$grid-width: 40px;
$gutter-width: 10px;

@function width($n) {
    @return $n * $grid-width + ($n - 1) * $gutter-width;
}

.container { 
    width: width(5); 
}

编译结果:

.container {
    width: 240px;
}

参考资料