css预处理器

2019年12月06日 阅读数:97
这篇文章主要向大家介绍css预处理器,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

sass 必须先安装ruby,再安装sass gem install sass
less 先安装npm npm install -g less
css

来自;http://blog.csdn.net/u013063153/article/details/52485993html

推荐连接:http://www.ruanyifeng.com/blog/2012/06/sass.html前端

CSS 预处理器是什么?通常来讲,它们基于 CSS 扩展了一套属于本身的 DSL,来解决咱们书写 CSS 时难以解决的问题:git

  • 语法不够强大,好比没法嵌套书写致使模块化开发中须要书写不少重复的选择器;
  • 没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,致使难以维护。

因此这就决定了 CSS 预处理器的主要目标:提供 CSS 缺失的样式层复用机制、减小冗余代码,提升样式代码的可维护性。这不是锦上添花,而偏偏是雪中送炭。github

网上已经有很多对比目前最主流的三个预处理器 Less、Sass 和 Stylus(按字母顺序排名)的文章了,可是彷佛都不是很详细,或者内容有些过期。下面我会更详细地探讨一下这三种预处理器的特性和它们的差别。web

下面主要会分为以下几方面来讨论:npm

  • 基本语法
  • 嵌套语法
  • 变量
  • @import
  • 混入
  • 继承
  • 函数
  • 逻辑控制

事先声明一下,平时我在开发中主要使用的是 Less,因此可能对 Sass 和 Stylus 的熟悉程度稍差一些,比较时主要参考三者官网的语言特性说明,有一些正在开发的功能可能会遗漏。编程

本文中对 CSS 语法的话术与 MDN 的 CSS 语法介绍一致。浏览器

基本语法

Less 的基本语法属于「CSS 风格」,而 Sass、Stylus 相比之下激进一些,利用缩进、空格和换行来减小须要输入的字符。不过区别在于 Sass、Stylus 同时也兼容「CSS 风格」代码。多一种选择在更灵活的同时,在团队开发中也免不了增长更多约定来保持风格统一。而对我的而言,语法风格按本身口味选择便可。sass

注:后面的 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 中,这个行为被称为「懒加载(Lazy Loading)」。全部 Less 变量的计算,都是以这个变量最后一次被定义的值为准。举一个例子更容易说清楚:

Less:

@size: 10px;
.box {
    width: @size;
}

@size: 20px;
.ball {
    width: @size;
}

输出:

.box {
  width: 20px;
}
.ball {
  width: 20px;
}

而在 Stylus 中:

size = 10px
.box
  width: size

size = 20px
.ball
  width: size

输出:

.box {
  width: 10px;
}
.ball {
  width: 20px;
}

Sass 的处理方式和 Stylus 相同,变量值输出时根据以前最近的一次定义计算。这其实表明了两种理念:Less 更倾向接近 CSS 的声明式,计算过程弱化调用时机;而 Sass 和 Stylus 更倾向于指令式。这两种方式会致使怎样的结果呢?

举个例子来讲,对于 Less,若是项目中引入了这样一个文件:

@error-color: #c00;
@success-color: #0c0;
.error {
  color: @error-color;
  background-color: lighten(@error-color, 40%);
}
.success {
  color: @success-color;
  background-color: lighten(@success-color, 40%);
}

在业务代码中,在不修改外部引入文件的状况下,若是我想重写这两种状态的配色,只须要从新配置 @error-color 和 @success-color 这两个变量,就能改变 .error 和 .success 的样式。

而在 Stylus 中,若是引入的第三方样式库中有这样的代码:

error-color = #c00
success-color = #0c0

.error
  color: error-color
  background-color: lighten(error-color, 40%)

.success
  color: success-color
  background-color: lighten(success-color, 40%)

这种状况下后面的代码就没法经过重写变量值来覆盖样式了。Sass 也是如此。优势是 Stylus 和 Sass 这样的处理会不容易受多个第三方库变量名冲突的影响,由于一个变量不能影响在定义它之前的输出样式。

因为 Sass 和 Stylus 变量在「运行」过程当中使用完能够修改后再使用输出不一样的值,因此这二者还提供了「仅当变量不存在时才赋值」的功能:

Sass:

$x: 1;
$x: 5 !default;
$y: 3 !default;

// $x = 1, $y = 3

Stylus:

x = 1
x := 5 // or x ?= 5
y = 3

// x = 1, y = 3

由于变量只能在输出前修改才能生效,因此若是要定制第三方库的样式,用户代码理论上得插入第三方库的配置与样式之间才能生效。而有了 !default,第三方库在提供默认配置时能够将开发给用户修改的变量设置为 !default,这样只要用户提早引入配置进行覆盖,就能够按需重写默认配置了:

// lib.scss
$alert-color: red !default;
.alert {
  color: $alert-color;
}
// var.scss
$alert-color: #c00;
// page.scss
@import var
@import lib

这样最终页面输出的效果就是被用户重定义过的内容了。

/* page.css */
.alert {
  color: #c00;
}

因为 Less 处理变量的方式,若是咱们要引入多个外部样式库或在多个团队进行合做开发时,若是不能确保开发过程可控,那为变量添加模块前缀就变得颇有必要。

此外,Sass 中提供一个 !global 的语法来让局部变量变成全局变量,也就是说 Sass 代码能够在内层覆盖全局变量的值。输出一段局部的样式可能使得后续全部样式都受到全局变量变化的影响。(这实际上是 Sass 开始时默认的逻辑,Sass 3.3 之前全部变量都是全局的,以后改为了和 Less 和 Stylus 同样有嵌套做用域,全局变量要显式指定 !global。)

插值

预处理器都有定义变量的功能,除了在最多见的属性值中使用,其余还有哪些地方能用变量来加强对样式的抽象、复用呢?

变量名插值

Less 中支持 @@foo 的形式引用变量,即该变量的名字是由 @foo 的值决定的。好比咱们能够利用它简化更清晰地调用 mixin:

// some icon font lib

// variables with prefix to prevent conflicts
@content-apple: "A";
@content-google: "G";

// clearer argument values
.icon-content(@icon) {
  @var: ~"content-@{icon}";
  &::before {
    content: @@var;
  }
}

.icon-apple {
  .icon-content(apple); // "A"
}

.icon-google {
  .icon-content(google); // "G"
}

选择器插值

选择器是样式表和 DOM 的纽带,是咱们实际暴露给 HTML 的接口。支持插值显然可让接口更不容易和其余内容冲突。假设咱们在开发一个 UI 库,生成的组件类名但愿有一个可配置的前缀,这时选择器插值就变得至关重要。初看下来,三者用法相似:

Less:

@prefix: ui;
.@{prefix}-button {
  color: #333;
}

Sass:

$prefix: ui
.#{$prefix}-button
  color: #333;

Stylus:

prefix = ui
.{prefix}-button
  color #333

可是在 Less 中,有一个很严重的问题:经过选择器插值生成的规则没法被继承(Extend dynamically generated selectors)!固然,若是有相似 Placeholder 的机制,这都不是事儿了。问题是 Less 没有!将来的方案看来多是经过 :extend(.mixin()) 的方式实现相似功能(:extend mixins)。虽然用 :extend 自己的语法说不过去,可是在现有机制上来看还算能够接受。关于样式的继承复用,后面会详细讲到。

@import 语句插值

Sass 中只能在使用 url() 表达式引入时进行变量插值:

$device: mobile;
@import url(styles.#{$device}.css);

Less 中能够在字符串中进行插值:

@device: mobile;
@import "styles.@{device}.css";

Stylus 中在这里插值无论用,可是能够利用其字符串拼接的功能实现:

device = "mobile"
@import "styles." + device + ".css"

注意因为 Less 的 Lazy Load 特性,即便是 @import 也是能够在后面的文件内容中进行覆盖的,修改掉变量就能够在前面引入不一样的外部文件。而 Sass 与 Stylus 一旦输出语句,就没法经过变量改变了。

属性名插值

三个预处理器的目前版本都支持属性名插值,用法也相似。这里仅以 Stylus 为例:

red-border(sides)
  for side in sides
    border-{side}-color: red // property name interpolation

.x
  red-border(top right)

输出:

.x {
  border-top-color: #f00;
  border-right-color: #f00;
}

其余 @ 规则插值

三种预处理器均支持在 @media@keyframes@counter-style 等规则中进行插值。@media 插值主要用来作响应式的配置,而 @keyframes 这样带名称名称的 @ 规则则能够经过插值来避免命名冲突。

Less:

@m: screen;
@orient: landscape;
@media @m and (orientation: @orient) {
  body {
    width: 960px;
  }
}

@prefix: ui;
@keyframes ~"@{prefix}-fade-in" {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

Sass:

$m: screen;
$orient: landscape;
@media #{$m} and (orientation: $orient) {
  body {
    width: 1000px;
  }
}

$prefix: ui;
@keyframes #{$prefix}-fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

Stylus:

m = screen
orient = landscape
mq = m + " and (orientation: " + orient + ")"
@media mq
  body
    width: 960px

vendors = official
prefix = ui;
@keyframes {prefix}-fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

三者均会输出以下 CSS:

@media screen and (orientation: landscape) {
  body {
    width: 960px;
  }
}
@keyframes ui-fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

Stylus 中彷佛有 and 时因为表达式计算的逻辑不能直接像 Less 与 Sass 那样写插值,因此这里采用了字符串拼接的方式。

@import

@import 对于模块化开发来讲很是有帮助,但就这个功能来讲,三种预处理器的行为各不相同。

先说 Less,Less 扩展了语法,为 @import 增长了多种选项:

  1. @import (less) somefile.ext

会将不管什么扩展名的文件都做为 Less 文件引入、一块儿编译;

  1. @import (css) somefile.ext

直接编译生成 @import somefile.ext,当作原生 @import

  1. @import (inline) somefile.ext

直接将外部文件拷贝进输出文件的这个位置,但不会参与编译;

  1. @import (reference) somefile.ext

外部文件参与编译,但不输出内容,仅用来被本文件中的样式继承;

  1. @import (optional) somefile.ext

引入文件但在文件不存在时不报错,静默失败。

上面的选项是能够联合使用的,好比能够这样写:

@import (less, optional) somefile.ext;

除此以外还有 once 和 multiple 选项分别用来表示去重和不去重的引入方式,默认为 once。在不写任何选项时,Less 会根据扩展名进行推断来决定引入逻辑。

Sass 没有扩展语法,而是本身推断引入的方式。.css 后缀、绝对路径、url() 表达式和带有 media query 的 @import 会直接用原生 @import,其余都会做为 Sass 代码参与编译。相比之下 Less 更灵活也更复杂。Sass 有个特有的功能叫作「partial」,由于 Sass 默认的编译工具能够编译整个目录下的文件,因此当一些文件不须要编译时,能够在文件名前加上 _ 代表这是一个被别的模块引入自己不须要编译的代码片断。Less 的 lessc 因为原本就只处理一个文件,因此这件事就交给用户本身去写编译脚本了。Sass 中有一个比较棘手的问题是,@import 不会被去重,屡次引入会致使一个样式文件被屡次输出到编译结果中。为了解决这个问题,Foundation 作了以下的 hack

// IMPORT ONCE
// We use this to prevent styles from being loaded multiple times for components that rely on other components.
$modules: () !default;

@mixin exports($name) {
  // Import from global scope
  $modules: $modules !global;
  // Check if a module is already on the list
  $module_index: index($modules, $name);
  @if (($module_index == null) or ($module_index == false)) {
    $modules: append($modules, $name) !global;
    @content;
  }
}

而后在定义样式时都调用 exports 这个 mixin 来输出,起到只输出一次的效果。

Stylus 和 Sass 比较接近,也使用隐性推断的方式,但在处理重复输出的问题上,Stylus 给出了一个自定义指令 @require,用法和@import 彻底同样,但只会输出一次。Stylus 还支持通配符,好比 @import 'product/*' 会引入 product 目录下的全部.styl 文件,但由于通常引入样式都要显式指定顺序,因此这个功能实用性不高。

三者相比较之下,Sass 的引入功能彷佛有点残缺,不能去重是很大的硬伤。虽然能用 Foundation 那种方式「解决」,但实际上这是语言自己应该解决的问题。

混入

混入(mixin)应该说是预处理器最精髓的功能之一了。它提供了 CSS 缺失的最关键的东西:样式层面的抽象。从语法上来讲,三种预处理器的差别也比较大,这甚至会直接影响到咱们的开发方式。

Less 的混入有两种方式:

  1. 直接在目标位置混入另外一个类样式(输出已经肯定,没法使用参数);
  2. 定义一个不输出的样式片断(能够输入参数),在目标位置输出。(注:后面如无特殊说明,mixin 均用来指代此类混入。

举例来讲:

.alert {
  font-weight: 700;
}

.highlight(@color: red) {
  font-size: 1.2em;
  color: @color;
}

.heads-up {
  .alert;
  .highlight(red);
}

最后输出:

.alert {
  font-weight: 700;
}
.heads-up {
  font-weight: 700;
  font-size: 1.2em;
  color: red;
}

能够混入已有类样式这一点很值得商榷。在上面的例子中,.alert 样式在被混入时甚至能够是 .alert();.highlight() 混入时也能够写成 .highlight;。那么咱们遇到这样的代码时根本不知道 alert 会不会是一个 HTML class。但因为这一点是在 Less 还不支持 extend 时就有的,因此也可以理解做者可能就是将这做为 extend 来用了。因此目前比较好的实践是:用代码规范规约开发者不得使用直接混入已有类样式的方式,而是先定义 mixin 而后在输出的类样式中进行调用,调用时必须显式加上 () 来代表这不是一个 class(事实上百度 EFE 已有的 Less 编码规范就是这么定义的)。继承则应该直接经过 Less 的 :extend 来实现。

另外须要注意的是,Less 在进行混入时,会找到全部符合调用参数的「mixin 签名」的样式一块儿输出。好比:

.mixin(dark; @color) {
  color: darken(@color, 10%);
}
.mixin(light; @color) {
  color: lighten(@color, 10%);
}
.mixin(@_; @color) {
  display: block;
}

@switch: light;
.class {
  .mixin(@switch; #888);
}

这个例子中,第二个和第三个 mixin 都匹配了调用时的参数,因而它们的规则都会被输出:

.class {
  color: #a2a2a2;
  display: block;
}

也就是说同名的 mixin 不是后面覆盖前面,而是会累加输出。只要参数符合定义,就会将 mixin 内部的样式规则、甚至变量所有拷贝到目标做用域下。

这一点一样会带来一个问题:若是存在和 mixin 同名的 class 样式,若是 mixin 没有参数则在调用时会把对应的 class 样式一块儿输出,这显然是不符合预期的。

假设有个叫 .clearfix 的 mixin,有两个 class 样式调用了它(其中一个也叫 clearfix):

.clearfix() {
  *zoom: 1;
  &:before,
  &:after {
    display: table;
    content: "";
  }
}

.clearfix {
  .clearfix();
}

.list {
  .clearfix();
}

获得的输出是:

.clearfix {
  *zoom: 1;
}
.clearfix:before,
.clearfix:after {
  display: table;
  content: "";
}
.clearfix:after {
  clear: both;
}
.list {
  *zoom: 1;
}
.list:before,
.list:after {
  display: table;
  content: "";
}
.list:after {
  clear: both;
}
.list:before,
.list:after {
  display: table;
  content: "";
}
.list:after {
  clear: both;
}

.list 的样式调用了两次!这一点在开发中必定要注意,不要给和非输出型 mixin 同名的类定义样式。

对于 Sass,语义很是明确:

@mixin large-text {
  font: {
    family: Arial;
    size: 20px;
    weight: bold;
  }
  color: #ff0000;
}

.page-title {
  @include large-text;
  padding: 4px;
  margin-top: 10px;
}

Sass 用 @mixin 和 @include 两个指令清楚地描述了语义,不存在混入类样式的状况,可是书写时略显繁琐一些。固然,用 Sass 语法 而非 SCSS 语法的话能够简单地用 = 定义 mixin,用 + 引入 mixin:

=large-text
  font:
    family: Arial
    size: 20px
    weight: bold
  color: #ff0000

.page-title
  +large-text
  padding: 4px
  margin-top: 10px

和 Less 不一样,同名的 mixin 能够覆盖以前的定义,做用机制相似变量。

Stylus 和 Sass 相似,但不用什么特殊的标记来引入:

border-radius(n)
  -webkit-border-radius: n
  -moz-border-radius: n
  border-radius: n

.circle
  border-radius(50%)

Stylus 中还有一个「透明 mixin」的功能,也就是说引入 mixin 彻底能够和引入普通属性同样!例如上面的这个 mixin,也能够这样引入:

.circle
  border-radius: 50%

这意味着能够把兼容性上的处理隐藏在 mixin 中,直接用标准属性同名的 mixin 按普通属性的方式输出。当不须要兼容老浏览器时,直接把 mixin 定义删除仍然可以正常输出。不过这种写法虽然感受很是「爽快」,但要求开发者必须能很好地区分原生属性和某个样式库中提供的 mixin 功能(对于有经验的开发者问题不大),并且透明意味着看到一个普通属性开发者不能判断是否已经在某处用 mixin 进行了重写,没法明确知道这里的代码最后输出会不会发生变化。在可控条件下,这个功能应该说是很是诱人的。

将声明块做为混入参数

若是说调用时想传入一组样式声明而非单个值,三种预处理器都提供了相应的功能,但实现方式各有不一样。

在 Less 中须要先定义一个「规则集变量」(detached ruleset,其实就是 CSS 声明块,即规则集去掉选择器的部分),而后在调用 mixin 时把它做为参数传进去,而后在 mixin 中用 @var() 的方式输出:

.red(@custom) {
  color: red;
  @custom();
}

.alert {
  @styles: {
    font-weight: 700;
    font-size: 1.5em;
  }

  .red(@styles);
}

在 Sass 和 Stylus 中,都支持直接在 mixin 调用下层传入声明块:

Sass 下直接跟一个声明块便可,而后用关键字 @content 来进行输出:

@mixin red() {
  color: red;
  @content;
}

.alert {
  @include red() {
    font-weight: 700;
    font-size: 1.5em;
  }
}

Stylus 支持两种方法,首先是 Less 那样的「具名」声明块,调用时当作变量:

red(foo)
  color: red
  {foo}

.alert
  foo =
    font-weight: 700
    font-size: 1.5em
  red(foo)

第二种是 Sass 那样相似传入「字面量」,而且用关键词 block 输出的方式。这种方式须要为要传入声明块的 mixin 前添加一个 +符号(多是来自 SCSS 的对应功能):

red()
  color: red
  {block}

.alert
  +red()
    font-weight: 700
    font-size: 1.5em

第二种方式能够看作是第一种方式的语法糖,在 mixin 只须要传入一个声明块时能够免去起名字带来的困扰。

相比之下 Less 只支持先定义变量后传入的方式,优势是能够传入多个声明块;而 Sass 只支持传入一个「匿名」声明块可是更简单;Stylus 则是两种方式都支持。这个功能在抽象「须要应用样式的条件」时很是有用,好比咱们基于 Stylus 的样式库 rider 中就用它来实现对 media query 的抽象封装

继承

混入很好用,可也有问题:若是多个地方都混入一样的代码,会形成输出代码的屡次重复。好比在 Stylus 下:

message()
  padding: 10px
  border: 1px solid #eee

.message
  message()

.warning
  message()
  color: #e2e21e

会输出:

.message {
  padding: 10px;
  border: 1px solid #eee;
}
.warning {
  padding: 10px;
  border: 1px solid #eee;
  color: #e2e21e;
}

而咱们可能指望的输出是:

.message,
.warning {
  padding: 10px;
  border: 1px solid #eee;
}
.warning {
  color: #e2e21e;
}

也许你们会说能够这么写:

message()
  padding: 10px
  border: 1px solid #eee

.message,
.warning
  message()

.warning
  color: #e2e21e

这样就能够按须要输出了。但其实预处理器的一个好处就是能够方便咱们进行模块化开发。上面的例子中,.message 和 .warning的样式若是是分布在两个模块中的,我合并过的选择器组样式写在哪里呢?状况更复杂的时候就更棘手了。

这个时候就该继承出场了:

.message
  padding: 10px
  border: 1px solid #eee

.warning
  @extend .message
  color: #e2e21e

这样就能够按模块进行开发(无论是分文件仍是在同一文件中按业务功能安排样式的顺序),同时兼顾输出的效率了。

Stylus 的继承方式来自 Sass,二者一模一样。 而 Less 则又「独树一帜」地用伪类来描述继承关系:

.message {
  padding: 10px;
  border: 1px solid #eee;
}

.warning {
  &:extend(.message);
  color: #e2e21e;
}
/* Or:
.warning:extend(.message) {
  color: #e2e21e;
}
*/

同时,Less 默认只继承父类自己的样式,若是要同时继承嵌套定义在父类做用域下的样式,得使用关键字 all,好比&:extend(.message all);

关于使用伪类描述继承关系,Hax 在 Less 的另外一个 issue 下曾经言辞激烈地提出了批评,同时也遭到了 Less 项目组绝不客气的回应。我我的彻底赞同 Hax 的见解,由于选择器是用来在树结构中找到元素的,和样式自己彻底无关。但 Less 社区在当时却对这个语法表示了一致的赞同,不由让人对其感到担心。

无论语法如何,继承功能还有一个潜在的问题:继承会影响输出的顺序。假设有以下的 Sass 代码:

.active {
   color: red;
}
button.primary {
   color: green;
}
button.active {
   @extend .active;
}

而对应的 HTML 代码是:

<button class="primary active">Submit</button>

很容易误觉得效果是红色的。而其实生成的 CSS 顺序以下:

.active, button.active {
  color: red;
}

button.primary {
  color: green;
}

因为合并选择器的关系 .active 被移到了 .primary 以前,因此依赖顺序而非选择器 specificity 时可能会遇到陷阱。

placeholder

Placeholder 是什么?简单来讲就是一个声明块(预处理器 DSL 中的声明块,包含其下嵌套规则),可是不会在最终的 CSS 中输出。其实这是一组「抽象」样式,只存在于预处理器的编译过程当中(相似 mixin),但不一样之处是它能够被继承。这样咱们就能够在纯样式层为声明块起与样式强耦合的名称而不怕它出如今 CSS 与 HTML 的「接口」——选择器之中了。

Sass:

%red-card {
  border: 1px solid #300;
  background-color: #ecc;
  color: #c00;
}

.alert {
  @extend %red-card;
}

Stylus:

$red-card
  border: 1px solid #300
  background-color: #ecc
  color: #c00

.alert
  @extend $red-card

均输出:

.alert {
  border: 1px solid #300;
  background-color: #ecc;
  color: #c00;
}

Less 目前不支持这个功能,但开发组目前的共识是可能会用继承 mixin 的方式来实现,好比上面的这个例子将来可能能够经过以下方法实现:

.red-card() {
  border: 1px solid #300;
  background-color: #ecc;
  color: #c00;
}

.alert {
  &:extend(.red-card());
}

当前在 Less 下也有一个 hack 来模拟 placeholder 功能,原理是利用 @import (reference) 来实现「placeholder」不输出的功能:

// placeholder.less
.red-card {
  border: 1px solid #300;
  background-color: #ecc;
  color: #c00;
}

// style.less
@import (reference) "placeholder.less";
.alert {
  &:extend(.red-card);
}

不过 @import (reference) 在复杂一些的状况下(被引入的文件有 @import、有 :extend 等)可能会遇到一些 bug,好比:#1851#1878#1896。目前以 reference 方式引入 Bootstrap 时就会直接产生代码输出。

函数

先说说原生函数。三种预处理器都自带了诸如色彩处理、类型判断、数值计算等内置函数,目前版本的数量都在 80 个左右。因为 Sass 和 Stylus 都内置脚本语言,因此自带函数中包括了不少处理不一样数据类型、修改选择器的函数。Sass 更是提供了很多特性检测函数好比feature-exists($feature)variable-exists($name) 等,这为第三方库的兼容性提供了很好的保障。由于有了这些函数能够方便地对不一样版本的 Sass 编译器有针对性地提供兼容,而不怕在老版本的编译环境中直接报错。

三者调用函数的方式几乎一致,不一样之处在于 Sass 和 Stylus 支持直接指定参数名的方式传入参数。以 Stylus 为例:

subtract(a, b)
  a - b

subtract(b: 10, a: 25) // same as substract(25, 10)

这样作的好处是,若是参数列表比较长,Stylus 能够直接为列表后面的参数赋值,而不须要一路将以前的参数填上 null 或默认值。Stylus 将这个特性称为「Named parameters」,而 Sass 称为「Keyword arguments」。

关于函数,真正的区别在于:Sass 和 Stylus 都支持用 DSL 直接添加自定义函数,而 Less 中若是要添加自定义函数必须经过使用插件(2.0.0 之后的版本才支持插件)。这决定了用 Sass 和 Stylus 书写的代码可移植性更高,不须要编译环境有插件便可运行,而 Less 则须要额外添加编译时的依赖。

Sass 中自定义函数须要使用 @function 指令,并用 @return 指令返回结果:

@function golden-ratio($n) {
  @return $n * 0.618;
}

.golden-box {
  width: 200px;
  height: golden-ratio(200px);
}

在 Stylus 中,这些都是隐含的,最后一个表达式的值会做为返回值:

golden-ratio(n)
  n * 0.618

.golden-box
  width: 200px
  height: golden-ratio(@width)

这种写法和 mixin 有什么区别?当把函数做为 mixin 调用时,若是其中有 prop: value 这样格式的内容,就会被当作样式规则输出。Stylus 中大量的内容都是根据调用时的 context 去隐式推断该使用什么逻辑进行输出,而非 Less 和 Sass 那样使用关键字去显式地进行区分。

逻辑控制

Sass 中经过常见的 @if@else if@else 实现条件分支,经过 @for@each@while 实现循环,配合 map 和 list 这两种数据类型能够轻松地实现多数编程语言提供的功能。

在 Stylus 中,不须要使用 @ 规则,提供了 ifelse ifelseunlessfor...in 来实现相似功能,语法比 Sass 更简洁灵活。

再来看 Less,上面说到的功能 Less 都没有提供。那在 Less 中如何进行逻辑控制呢?Less 中只有一个方式:使用 mixin。

Less 中的 mixin 经过「guard」的方式支持简单的条件分支控制。好比咱们要实现一个控制 ::placeholder 样式的 mixin,当传入颜色时只设置颜色,当传入声明块时输出对应的样式规则,其余状况输出一个默认的 color

.mixin(@val) when (iscolor(@val)) {
  color: @val;
}
.mixin(@val) when (isruleset(@val)) {
  @val();
}
.mixin(@val) when (default()) {
  // default() in guards acts as else
  color: #666;
}

Guard 语句中的语法很是相似 CSS media query 中的对应功能,事实上这也是 Less 一直以来的理念:保持声明式语法,弱化指令和流程。可是事实上,这为书写须要提供灵活接口的样式库形成了很是大的不便。最简单的三元表达式在 Less 中也须要先定义一个 mixin,根据判断条件写对应的 guard 表达式,而后再输出。

而对于循环,Less 自己并不支持。但官网给出了一个使用 mixin 递归调用模拟循环的例子:

.loop(@counter) when (@counter > 0) {
  .loop((@counter - 1));    // next iteration
  width: (10px * @counter); // code for each iteration
}

div {
  .loop(5); // launch the loop
}

编译结果为:

div {
  width: 10px;
  width: 20px;
  width: 30px;
  width: 40px;
  width: 50px;
}

这是一种很是别(dàn)扭(téng)的实现方式,但从 Less 开发团队的态度来看,将来并没什么可能在 Less 中见到真正的条件分支和循环——由于「Less 不是 Sass」。

因为逻辑处理能力不能与 Sass 和 Stylus 相比,因此在 Less 中可能还会须要借助 JS 表达式来进行 mixin 参数的解析处理。这个功能 Less 官方已是不推荐使用的了(已经从官网文档中移除)——由于使用这一功能也会致使 Less 代码的可移植性变低,由于直接内嵌 JS 代码,因此没法使用 dotless 等其余语言的 Less 编译器进行处理。并且不一样 JS 引擎还可能有兼容性差别。

总结

我我的认为,Less 从语言特性的设计到功能的健壮程度和另外二者相比都有一些缺陷,但由于 Bootstrap 引入了 Less,致使 Less 在今天仍是有不少用户。用 Less 能够知足大多数场景的需求,但相比另外二者,基于 Less 开发类库会复杂得多,实现的代码会比较脏,能实现的功能也会受到 DSL 的制约。比 Stylus 语义更清晰、比 Sass 更接近 CSS 语法,使得刚刚转用 CSS 预编译的开发者可以更平滑地进行切换。当初 Sass 并不支持 SCSS 语法,使得转投 Sass 成本较高,因此 Alexis Sellier 才萌生开发一个更「CSS」的预处理器的念头。大获成功之后反过来影响到了 Sass,迫使其也支持相似 CSS 语法的 SCSS。另外,Less 支持浏览器端编译,这无疑下降了开发门槛,使得不少非专业的开发者可以更快地上手(对于一些我的项目来讲,能让项目跑起来就行,对前端的性能并无专业工程师那么高的要求)。

Sass 在三者之中历史最久,也吸取了其余二者的一些优势。从功能上来讲 Sass 大而全,语义明晰可是代码很容易显得累赘。主项目基于 Ruby 可能也是一部分人不选择它的理由(Less 开始也是基于 Ruby 开发,后来逐渐转到 less.js 项目中)。 Sass 有一个「事实标准」库——Compass,因而对于不少开发者而言省去了选择类库的烦恼,对于提高开发效率也有不小的帮助。

Stylus 的语法很是灵活,不少语义都是根据上下文隐含的。基于 Stylus 能够写出很是简洁的代码,但对使用团队的开发素养要求也更高,更须要有良好的开发规范或约定。Stylus 是前 Node.js 圈第一大神 TJ Holowaychuk 的做品,虽然他已经弃坑了,可是仍然有不小的号召力。和 Sass 有 Compass 相似,Stylus 有一个官方开发的样式库 nib,一样提供了很多好用的 mixin。对于比较有经验的开发者,用 Stylus 可能更会有一种畅快的感受。总的来讲用一个词形容 Stylus 的话,我会用「sexy」。

总的来讲,三种预处理器百分之七八十的功能是相似的。Less 适合帮助团队更快地上手预处理代码的开发,而 Sass 和 Stylus 的差别更在于口味。好比有的人喜欢 jQuery 用一个 $ 作大部分的事,而另外一些人以为不同的功能就该有明确的语义上的差异。在这里我不会作具体的推荐。固然,再次声明一下因为我我的接触 Less 开发比较多,因此可能遇到的坑也多一些,文中没有列出 Sass 和 Stylus 的问题并不表明他们没有。

最后打个广告:百度 EFE 目前有一个基于 Less 的样式库 est,以及一个基于 Stylus 的针对移动端的样式库 rider,欢迎你们关注、提交 issue 和 pull request。