CSS 预处理器是什么?一般来说,它们基于 CSS 扩展了一套属于自己的 DSL,来解决我们书写 CSS 时难以解决的问题:
- 语法不够强大,比如无法嵌套书写导致模块化开发中需要书写很多重复的选择器;
- 没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护。
所以这就决定了 CSS 预处理器的主要目标:提供 CSS 缺失的样式层复用机制、减少冗余代码,提高样式代码的可维护性。这不是锦上添花,而恰恰是雪中送炭。
网上已经有不少对比目前最主流的三个预处理器 Less、Sass 和 Stylus(按字母顺序排名)的文章了,但是似乎都不是很详细,或者内容有些过时。下面我会更详细地探讨一下这三种预处理器的特性和它们的差异。
下面主要会分为如下几方面来讨论:
- 基本语法
- 嵌套语法
- 变量
- @import
- 混入
- 继承
- 函数
- 逻辑控制
事先声明一下,平时我在开发中主要使用的是 Less,所以可能对 Sass 和 Stylus 的熟悉程度稍差一些,比较时主要参考三者官网的语言特性说明,有一些正在开发的功能可能会遗漏。
本文中对 CSS 语法的话术与 MDN 的 CSS 语法介绍一致。
基本语法
Less 的基本语法属于「CSS 风格」,而 Sass、Stylus 相比之下激进一些,利用缩进、空格和换行来减少需要输入的字符。不过区别在于 Sass、Stylus 同时也兼容「CSS 风格」代码。多一种选择在更灵活的同时,在团队开发中也免不了增加更多约定来保持风格统一。而对个人而言,语法风格按自己口味选择即可。
注:后面的 Sass 代码会用被更多人接受的 SCSS 风格给出。
Less & SCSS:
.box { display: block; } |
Sass:
.box display: block |
Stylus:
.box display: block |
嵌套语法
三者的嵌套语法都是一致的,甚至连引用父级选择器的标记 &
也相同。区别只是 Sass 和 Stylus 可以用没有大括号的方式书写。以 Less 为例:
.a { &.b { color: red; } } |
生成的 CSS 为:
.a.b { color: red; } |
除了规则集的嵌套,Sass 额外提供了一个我个人认为比较另(jī)类(lèi)的「属性嵌套」:
.funky { font: { family: fantasy; size: 30em; weight: bold; } } |
选择器引用
三者都支持用 &
在嵌套的规则集中引用上层的选择器,这可以是嵌套书写 CSS 时的「惯例」了。语法相同,但是逻辑上有些许差异。在一个选择器中用两次以上 &
且父选择器是一个列表时,Less 会对选择器进行排列组合,而 Sass 和 Stylus 不会这么做。
也就是说,假设上层选择器为 .a, .b
,则内部的 & &
在 Less 中会成为 .a .a, .a .b, .b .a, .b .b
,而 Sass 和 Stylus 则输出 .a .a, .b .b
。
假设我们要用预处理器书写 WHATWG 推荐的 section 标题样式,在 Less 中可以方便地书写为:
article, aside, nav, section { h1 { margin-top: 0.83em; margin-bottom: 0.83em; font-size: 1.50em; } & & h1 { margin-top: 1.00em; margin-bottom: 1.00em; font-size: 1.17em; } & & & h1 { margin-top: 1.33em; margin-bottom: 1.33em; font-size: 1.00em; } & & & & h1 { margin-top: 1.67em; margin-bottom: 1.67em; font-size: 0.83em; } & & & & & h1 { margin-top: 2.33em; margin-bottom: 2.33em; font-size: 0.67em; } } |
当然,这个推荐样式十分脑残,编译出来的结果会有 47KB 之巨,根本不可用,这里只是借来演示一下。
除了 &
,Sass 和 Stylus 更进一步,分别用 @at-root
和 /
符号作为嵌套时「根」规则集的选择器引用。这有什么用呢?举个例子,假设 HTML 结构是这样的:
<article class="post"> <h1>我是一篇文章</h1> <section> <h1 class="section-title"><a href="#s1" class="section-link">#</a>我是章节标题</h1> <p>我只是一个<em>例子</em>。</p> </section> </article> |
如果我这么写 Sass 代码,是完全符合业务的嵌套关系的:
.post { section { .section-title { color: #333; .section-link { color: #999; } } /* other section styles */ } /* other post styles */ } |
但是这样生成出来的选择器会有 .post section .section-title .section-link
,很多时候我们觉得写成 .post .section-link
就够了。
于是我们在 Stylus 中可以这么写:
.post section .section-title color #333 /.post .section-link color #999 /* other section styles */ /* other post styles */ |
这样输出的 CSS 就会是:
.post section .section-title { color: #333; } .post .section-link { color: #999; } |
这就是我们想要的样子了。当然也可以这样写:
.post section .section-title color #333 /* other section styles */ .section-link color #999 /* other post styles */ |
我个人是推荐这种写法(不使用 root 引用)的,因为当你确定 .section-link
的样式不依赖于它位于 section
或 .section-title
下时,就不应该嵌套于此。否则如果为了一点点性能上的考虑(还不一定会是优化),使得设计意图变得更不准确,我觉得得不偿失。
变量
变量无疑为 CSS 增加了一种有效的复用方式,减少了原来在 CSS 中无法避免的重复「硬编码」。
Less:
@red: #c00; strong { color: @red; } |
Sass:
$red: #c00; strong { color: $red; } |
Stylus:
red = #c00 strong color: red |
Less 的选择有一个问题:@ 规则在 CSS 中可以算是一种「原生」的扩展方式,变量名用 @
开头很可能会和以后的新 @ 规则冲突。(当然理论上只要 CSS 规范不引入 @a: b
这样的规则,问题也不大。而且规范制定的时候也会参考很多现有的实现。)
相比之下 Sass 的选择中规中矩,而 Stylus 就不同了,不需要额外的标志符。这意味着:在 Stylus 中,我们可以覆写 CSS 原生的属性值!Stylus 的设计让人有一种「你以为你在写 CSS,但其实你不是」的感觉,后面会有更多这样的例子。
顺便说一下,CSS 规范也有关于变量实现的草案,目前的方案是这个样子的:
/* global scope */ :root { --red: #c00; } strong { color: var(--red); } |
不管语法槽点如何,原生 CSS 变量可以通过 DOM 结构来继承,也就是说是代码真正「运行」时(runtime)决定的。元素引用一个变量时会按 DOM 向上查找定义在上层元素上的同名变量。这一点是任何预处理语言都无法做到的。可以用 Firefox 31+ 看一下这个 demo。至于这种机制是不是好用,暂时还没研究过。不过从开发的思维惯性来看,还很难一下子适应这种方式。
变量作用域
三种预处理器的变量作用域都是按嵌套的规则集划分,并且在当前规则集下找不到对应变量时会逐级向上查找,注意这个和原生 CSS 的逻辑是完全不同的。
如果我们在代码中重写某个已经定义的变量的值,Less 的处理逻辑和其他两者有非常关键的区别。在 Less 中,这个行为被称为「