CSS工程化演進
CSS 技術的演進
CSS 是 Web 開發中不可或缺的一部分,在前端工程化的不斷進步的今天,一方面在 CSS 特性隨著規范的升級越來越豐富,另一方面,前端業務復雜性的增加帶來的工程愈加龐大,驅使者開發者不斷尋找CSS工程化的最佳實踐。
Web開發模塊化趨勢
不可否認,無論從現代前端框架(React,Vue,Angular,Polymer等),還是從W3C的 Web Components 草案看來,組件化已經是前端開發的主流之選和未來的發展方向,正如在 reddit
上有網友說道 "非死book.com's codebase includes over 20,000 components"。廣義上看,所有頁面上都可以被劃分成一個個組件,相對于過去以網頁作為開發單位,以組件為單位開發有著可復用,可擴展等等一系列有利于項目工程化的優點。
在這種組件化趨勢的背景下,CSS 模塊化也漸漸有有著各種嘗試。
預處理與后處理
預處理
比較流行的CSS預處理器有 Sass
, Less
和 Stylus
,CSS 預處理器的出現主要針對于 CSS 缺少編程語言的靈活性而生的,是引入了一些編程概念而生的 DSL,開發者編寫簡介的語義化 DSL 代碼,由預處理器編譯成 CSS。
以 Sass
為例,該預處理器支持 .scss
, .sass
文件類型,其語法支持變量、選擇器嵌套、繼承(extend)、混合(mixin)和一些邏輯語句,同時還支持跨文件的導入功能,因而使得開發者能夠很好的使用編程思想書寫樣式。
從實際使用情況來看,幾個預處理器各有優缺點,社區活躍度上看 Sass > Less > Stylus,在于 Sass 是三個中間最早也是最成熟的,因而有著很多開源積累和很好編程范式,像內置了很多 Sass 的函數的 compass 框架,就是一個很好的例子;Less 相對于 Sass 的優點在于十分的輕量,也完全兼容 CSS,相對于但另一方面可編程能力也不如 Sass, Bootstrap 最新版本的 CSS 預處理器也從 Less 換成 Sass;Stylus 是來源于 node 社區,使用體驗上并不輸于 Sass 和 Less,無論是編譯速度還是語法范式,個人看來,stylus 在某種程度上更加優于其他兩個。
后處理
后處理器是對原生 CSS 進行處理并最終生成 CSS 的處理器,廣義上還是個預處理器,與上面不同的是,它處理的對象是標準 CSS,比較典型的后處理工具有:
圖1
PostCSS
PostCSS
一開始是從 AutoPrefixer
項目中抽象出來的框架,它本身并不對CSS做具體的業務操作,只是將CSS解析成抽象語法樹(AST),樣式的操作由之后運行的插件系統完成。正如其本身所言“Tra nsforming styles with JS plugins”
圖2
更多時候我們討論的 PostCSS ,并不止是其解析 CSS 的核心工具,更包括它創建的插件系統,而今 PostCSS 最為吸引開發者的正是其擴展性較強的插件系統和豐富的插件支持。
常用的插件
- autoprefixer -- 自動補全CSS屬性兼容性前綴
- postcss-cssnext -- 使用最新的 CSS 語法
- postcss-modules -- 組件內自動關聯樣式至選擇器
- stylelint -- CSS 語法檢查器
...
當然,如果已有的插件不能滿足現有的需求,完全可以 手寫一個插件 :
// 示例 rem 轉 px
var custom = function(css, opts){
css.eachDecl(function(decl){
decl.value = decl.value.replace(/\d+rem/, function(str){ return 16 * parseFloat(str) + "px";
});
});
};
當然,PostCSS 的解析并不局限于 CSS,結合它提供的 自定義 語法解析接口,完全可以定義自己的語法。其實類似于 postcss-scss 的插件社區已經有很多了,使用這些插件,可以將原來基于 SASS
, LESS
等預處理器的代碼遷移成至 PostCSS。相對于傳統的預處理器,PostCSS這種開放平臺型的體系,能夠不拘束開發者的開發方式,同時也促進了更多對于 CSS 解決方案的探索。
回過頭來看,為什么會有對于 CSS 的預處理操作后處理操作 ?其實主要的原因在于前端項目的膨脹使得用傳統手工編寫并維護 CSS 變得很不堪,根本上由于 CSS 沒有缺少編程語言特性,要做到對于 CSS 代碼的模塊化以及高復用的抽象處理,就必須引入一些編程的思想。相對于 JS 標準推進以及基礎設施的完備,CSS 在于編程方面的探索更多來自于社區,也并無統一的事實標準,這也是 CSS 發展落后于JS的原因。
namespace 約束
一方面我們需要關注技術能夠帶來代碼上的模塊化,另一方面我們又要思考如何使用一個良好的風格架構起項目中的 CSS。CSS 除了代碼外,另一個很重要的就是 CSS 選擇標記,但 CSS 選擇器的命名空間是全局的,并沒有局部的概念,因而如何利用好這個全局的空間,選擇良好的結構風格,也是在開發過程中必須考慮的。
OOCSS
OOCS (Object-Oriented CSS)即面向對象 CSS,主要有兩個核心原則
-
分離結構和皮膚(separate structure and skin)
皮膚即一些重復的視覺特征,如邊框、背景、顏色,分離是為了更多的復用;結構是指元素大小特征,如高度,寬度,邊距等等。
.button {
padding: 10px;
box-shadow: rgba(0, 0, 0, .5) 2px 2px 5px;
}
.widget {
overflow: auto;
box-shadow: rgba(0, 0, 0, .5) 2px 2px 5px;
}
根據此原則,我們需要對公用的皮膚進行提取并分離,如下
.button {
padding: 10px;
}
.widget {
overflow: auto;
}
.skin {
box-shadow: rgba(0, 0, 0, .5) 2px 2px 5px;
}
-
分離容器和內容(separate container an content)
打破容器內元素對于容器的依賴,元素樣式應該獨立存在。
舉個例子
<div class="container"><h2>xxx</h2></div>
.container h2 {...}
上面的 h2
元素依賴于父元素 container
, 對應此原則, h2
元素需要使用一個單獨的選擇器,如下
<div class="container"><h2 class="category">xxx</h2></div>
.category {...}
從實踐中看出,使用 OOSCC
范式,遵守了 DRY 的原則,能夠大量減少重復的樣式代碼,提高代碼復用;同時,視覺元素可以意靈活組合各個類名,展示不同的效果,豐富的類名也同時使得元素有著更好的可讀性;另一方面,由于容器和內容的分離,CSS 完成了與 HTML 結構解耦。
但同時也會帶來一些缺點,抽象復用會使class越來越多,極端情況會產生可能產生很多原子類,這對于那些偏向于“單一來源原則”的開發者來說并不受歡迎。
SMACSS
SMACSS (Scalable and Modular Architecture for CSS) 即模塊化架構的可擴展CSS,它主要是將規則分為5類
-
基礎(Base)
tag select
的樣式,定義最基礎全局樣式,如CSS REST
。
html, body, form { margin: 0; padding: 0; }
a { color: #039; } a:hover { color: #03C; }
-
布局(Layout)
將頁面分為各個區域的元素塊
.header{}
....
.footer{}
-
模塊(Module)
可復用的單元。在模塊中需要注意的是選擇器一律選擇
class selector
,避免嵌套子選擇器,減少權重,方便外部覆蓋。
<div class="pod pod-constrained">...</div>
<div class="pod pod-callout">...</div>
.pod { width: 100%; }
.pod .pod-callout { width: 200px; }
.pod .pod-constrained{}
-
狀態(State)
狀態 class 一般通過js動態掛載到元素上,可以根據狀態覆蓋元素上特定屬性。
.tab { background-color: purple;... }
.is-tab-active { background-color: white; }
-
主題(Theme)
可選的視覺外觀。一般根據需求有顏色,字體,布局等等,實現是將這些樣式單獨抽出來,根據外部條件( data 屬性,媒體查詢等)動態設置。
SMACSS 的主要優點在于按照不同的業務邏輯,將整個 CSS 結構化分更加細致,約束好命名,最小化深度,在編寫的時候,使用SMACSS規范能夠更好的組織好 CSS 文件結構和 class 命名。
BEM
BEM 即 Block Element Modifier
;類名命名規則: Block__Element--Modifier
- Block 所屬組件名稱
- Element 組件內元素名稱
- Modifier 元素或組件修飾符
其核心思想就是組件化。首先一個頁面可以按層級依次劃分未多個組件,其次就是單獨標記這些元素。BEM通過簡單的塊、元素、修飾符的約束規則確保類名的唯一,同時將類選擇器的語義化提升了一個新的高度。
<form class="form form--theme-xmas form--simple">
<input class="form__input" type="text" />
<input
class="form__submit form__submit--disabled"
type="submit" />
</form>
.form { }
.form--theme-xmas { }
.form--simple { }
.form__input { }
.form__submit { }
.form__submit--disabled { }
BEM 通過簡單的命名規則使得關聯類名元素語義性、可讀性更強,利于項目管理和多人協作;同時 BEM 方案中并沒有嵌套,所有類名最淺深度,并不會出現嵌套過深難以覆蓋的情況,易于維護、復用;
另一方面,BEM 強調單一職責原則和單一樣式來源原則,意味著傳統純手工 CSS 可能會產生大量重復的代碼,但是結合各種 CSS 預處理和 PostCSS 就可以很好的避免問題的產生。另外,雖說股則簡單,但在實際使用中,維護 BEM 的命名確實需要一些成本,很多時候命名反而成了一件難事。
CSS IN JS
css in js
的方案一開始是由 非死book 工程師 vjeux 在一次分享中提出的,針對于 CSS 在React開發中遇到的各種問題,隨后社區涌現了各種各樣的方案。
雖然以上模塊化的命名約定可以解決風格上的問題,但正如上面而言,也引入一些成本。而對于一些高復用的組件,使用以上高度語義化的方案是個很好的選擇,這種成本是必需的,但對于沒有復用的業務組件來說,顯然這種命名的成本大于收益,特別是在多人協作時候,另外面對現代前端框架的發展,純靠 CSS 方案并不能很好的解決。
CSS modlue
CSS module
不同于 vjeux 的完全放棄 CSS,它只是選擇了用 js 來管理樣式與元素的關聯, CSS Module
通過為每個本地定義的類名動態創建一個全局唯一類名,然后注入到UI上,實現編寫樣式規則的局部模塊化。
css-loader
內置支持 css-module
,只需設置下查詢參數,即可在 JS
中使用CSS文件的導入:
{
loader: 'css-loader',
query: {
module: true,
localInentName: '[name]__[local]--[hash:base64:5]' //
}
}
在 JS 中導入 css 文件,最終得到的其實是一個經過 CSS 文件進過 parse
后生成的類名映射對象 {[localName]: [hashed-Name], ....}
// Header.jsx
import style from './Header.css'
...
console.log(style) // {header: 'Header__header--3kSIq_0'}
export default () => <div className={style.header}></div>
同時 CSS 文件也會被編譯成對應的類名
.Header__header--3kSIq_0- {} // from Header.css .header{}
從開發體驗上看, CSS-Module
這種做法讓開發者不必在類名的命名上小心翼翼,直接使用隨機編譯生成唯一標識,讓類名的成為局部變量成為了可能。但同時因為也因為隨機性,失去了通過此局部類名實現樣式覆蓋的可能性,覆蓋時不得不考慮使用其他選擇器(如屬性選擇器)。對于復用的組件而言,靈活性是必不可少的,這種局部模塊化方案并不適合這種高度抽象復用的組件,而對于一次性業務組件確實能夠提升開發效率。
同時 CSS module
還支持使用 composes
實現CSS代碼的組合復用。
/* button.css */
.base{}
.normal {
composes: base
...
}
// button.jsx
import style from './button.css'
export default () => <button className={style.normal}>按按</button> // <button class="button__base--180HZ_0 button__normal--x38Eh_0">按按</button>
當然 CSS-module
還可以配合各種預處理器一起使用,只需在 css-loader
之前添加對于的 loader
,但是在編寫的時候要注意 CSS-module
的語法要在處理器之后合法。實際使用中,對于 CSS 代碼的解耦,如果引入了預處理器,代碼文件的模塊化就不建議使用 composes
來解決。
styled-components
styled-components 也是一個完全的 css-in-js
方案,先看語法:
// button
import styled from 'styled-compenents'
const Button = styled.button`
padding: 10px;
${props => props.primary ? 'palevioletred' : 'white'};
`
<Button>按鈕</Button>
<Button primary>按鈕</Button>
其編譯后也是如同 CSS-in-module
一樣,隨機混淆生成全局唯一類名,對應生成CSS文件。styled-component 的核心是“樣式即組件”,將字符串解析成CSS,并創建對應該樣式的 JSX 元素,而有著JS強大的編程能力,完全可以勝任類似于,同時讓組件樣式與組件邏輯耦合在一起,真正做到組件緊耦合少依賴。當然有些開發不喜歡這種耦合,也完全可以將樣式組件和邏輯組件分離,而在JS中分離代碼本身也是件易事。
當然, styled-components
在真正的應用并不僅僅如此,它完全是一個完備的樣式解決方案,有著如擴展、主題、服務端渲染、Babel 插件、React-Native 等等一系列支持,也深受一些開發者歡迎。這里比較有趣是看似奇怪的語法形式,其實是 ES6 中模板字符的特性。
styled-components
本身是React社區針對于JSX產生的一種方案,當然在 Vue 中通過 vue-styled-components 也能使用該功能,但是使用體驗一般,無論是在模板里還是在JSX中,使用組件都需在提前聲明并注入到組件構建參數中,十分繁瑣,而且不同于 React 純 JSX 的組件渲染語法, Vue 中并不能對既有的組件使用 styled
語法。
但另一方面,將 CSS 完全寫在 JS 中,社區里中也有很多人持反對態度, react-css-modules 的作者就專門 發文 表示反對 styled-component
這種完全拋棄 CSS 文件的開發模式。
總結
我們在開發的之前,面對各種技術方案,一定要選取并組合出最適合自己項目的方案,是選用傳統的CSS預處理器, 還是選用 PostCSS? 是全局手動維護模塊,還是完全交個程序隨機生成類名?都需要結合業務場景、團隊習慣等等因素。另一方面,CSS 本身并無編程特性,但在其工程化技術的發展中缺不乏很多優秀的編程思想,無論是自定義的 DSL 還是基于 JS,這其中帶給我們思考的正是“編譯思想”。
來自:https://zhuanlan.zhihu.com/p/32117359