再谈 CSS 预处理器
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 = #c00strong
?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 = #c00success-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 = 1x := 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 = screenorient = 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);
}
最后輸出:
?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 簽名」的樣式一起輸出。比如:
?color: darken(@color, 10%);
}
.mixin(light; @color) {
?color: lighten(@color, 10%);
}
.mixin(@_; @color) {
?display: block;
}
@switch: light;
.class {
?.mixin(@switch; #888);
}
這個例子中,第二個和第三個 mixin 都匹配了調用時的參數,于是它們的規則都會被輸出:
?color: #a2a2a2;
?display: block;
}
也就是說同名的 mixin 不是后面覆蓋前面,而是會累加輸出。只要參數符合定義,就會將 mixin 內部的樣式規則、甚至變量全部拷貝到目標作用域下。
這一點同樣會帶來一個問題:如果存在和 mixin 同名的 class 樣式,如果 mixin 沒有參數則在調用時會把對應的 class 樣式一起輸出,這顯然是不符合預期的。
假設有個叫 .clearfix 的 mixin,有兩個 class 樣式調用了它(其中一個也叫 clearfix):
?*zoom: 1;
?&:before,
?&:after {
? ?display: table;
? ?content: "";
?}
}
.clearfix {
?.clearfix();
}
.list {
?.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,語義非常明確:
?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:
和 Less 不同,同名的 mixin 可以覆蓋之前的定義,作用機制類似變量。
Stylus 和 Sass 類似,但不用什么特殊的標記來引入:
?-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 中,不需要使用 @ 規則,提供了 if、else if、else、unless、for...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 的問題并不代表他們沒有。
轉載于:https://blog.51cto.com/xhtml/1682628
總結
以上是生活随笔為你收集整理的再谈 CSS 预处理器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 尚硅谷公开课--struts2--2--
- 下一篇: 如何使用 Docker 部署一个基于 P