再談CSS 預處理器

再談CSS 預處理器
2016-09-09 Justineo JavaScript
轉自:http://efe.baidu.com/blog/revisiting-css-preprocessors/

CSS 預處理器是什么?一般來說,它們基于 CSS 擴展了一套屬于自己的 DSL,來解決我們書寫 CSS 時難以解決的問題:

  • 語法不夠強大,比如無法嵌套書寫導致模塊化開發中需要書寫很多重復的選擇器;
  • 沒有變量和合理的樣式復用機制,使得邏輯上相關的屬性值必須以字面量的形式重復輸出,導致難以維護。

所以這就決定了 CSS 預處理器的主要目標:提供 CSS 缺失的樣式層復用機制、減少冗余代碼,提高樣式代碼的可維護性。這不是錦上添花,而恰恰是雪中送炭。

網上已經有不少對比目前最主流的三個預處理器 Less、Sass 和 Stylus(按字母順序排名)的文章了,但是似乎都不是很詳細,或者內容有些過時。下面我會更詳細地探討一下這三種預處理器的特性和它們的差異。

下面主要會分為如下幾方面來討論:

  • 基本語法
  • 嵌套語法
  • 變量
  • @import
  • 混入
  • 繼承
  • 函數
  • 邏輯控制

事先聲明一下,平時我在開發中主要使用的是 Less,所以可能對 Sass 和 Stylus 的熟悉程度稍差一些,比較時主要參考三者官網的語言特性說明,有一些正在開發的功能可能會遺漏。

基本語法

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)的「屬性嵌套」:

    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 代碼可以在內層覆蓋全局變量的值。輸出一段局部的樣式可能使得后續所有樣式都受到全局變量變化的影響。

插值

預處理器都有定義變量的功能,除了在最常見的屬性值中使用,其他還有哪些地方能用變量來增強對樣式的抽象、復用呢?

變量名插值

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}-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 增加了多種選項:
@import (less) somefile.ext
會將無論什么擴展名的文件都作為 Less 文件引入、一起編譯;
@import (css) somefile.ext
直接編譯生成 @import somefile.ext,當做原生 @import;
@import (inline) somefile.ext
直接將外部文件拷貝進輸出文件的這個位置,但不會參與編譯;
@import (reference) somefile.ext
外部文件參與編譯,但不輸出內容,僅用來被本文件中的樣式繼承;
@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 的混入有兩種方式:
直接在目標位置混入另一個類樣式(輸出已經確定,無法使用參數);

定義一個不輸出的樣式片段(可以輸入參數),在目標位置輸出。(注:后面如無特殊說明,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-exits(feature)、variable-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 那樣使用關鍵字去顯式地進行區分。

以下內容超出了微信字數限制,遂用圖片代替,體驗估計不怎么好,建議點擊圖片放大查看


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容