在Facebook裡,上千名工程師工作在不同的產品線上,為全世界的使用者提供可靠優質的服務,而我們在程式碼質量管理方面也面臨著獨一無二的挑戰。不僅僅是因為我們面對的是一個龐大的程式碼基庫,還有日漸增加的各種各樣的特性,有時候如果你想去重構提高某一個模組,往往會影響到其他很多模組。具體在CSS而言,我們需要處理上千份不停變化的CSS檔案。之前我們著力於通過Code Review、程式碼樣式規範以及重構等手段協同工作,而保障程式碼質量,但是還是會有很多的錯誤悄悄從眼皮底下溜走,被提交進入到程式碼庫裡。我們一直用自建的CSS Linter來檢測基本的程式碼錯誤與保證一致的編碼風格,儘管它基本上已經滿足了我們的目標,但還是存在很多的問題,因此我也想在這篇文章裡對如何保障CSS的程式碼質量進行一些討論。
Regex is not Enough:之前用的是正則匹配,不咋的啊
老的Linter主要是基於很多個正規表示式對CSS中的語法進行提取,大概是這個樣子的:
1 2 3 4 5 6 7 8 9 10 |
preg_match_all( // This pattern matches [attr] selectors with no preceding selector. '/\/\*.*?\*\/|\{[^}]*\}|\s(\[[^\]]+\])/s', $data, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); foreach ($matches as $match) { if (isset($match[1])) { raiseError(...); } |
基本上一個檢測規則就需要新增一個專門的匹配規則,非常不好維護,在效能上也有很大的問題。對於每個規則俺們都需要遍歷整個檔案,效能差得很。
Abstract Syntax Tree
受夠了正規表示式,我們想搞一個更好用的也是更細緻的CSS直譯器。CSS本身也是一門語言,老把它當做純文字檔案處理也不好,因此我們打算用AST,即抽象語法樹的方式構建一個解析器。這種新的處理方式在效能上面有個很不錯的提升,譬如我們的程式碼庫中有這麼一段CSS程式碼:
1 2 3 4 5 6 |
{ display: none: background-color: #8B1D3; padding: 10px,10px,0,0; opacity: 1.0f; } |
眼神好的估計才能看出這個程式碼片段中存在的問題,譬如某個屬性名錯了、十六進位制的顏色程式碼寫錯的,分隔符寫錯了等等。瀏覽器才不會主動給你報錯呢,這樣開發者自己也就很難找到錯誤了。在具體實現上,我們發現PostCSS 是個不錯的工具,因此我們選擇了Stylelint作為我們新的Linter工具,它是基於PostCSS構建的,非常的靈活,社群也不錯。
就像JavaScript中的Esprima以及ESLint一樣,Stylelint提供了對於完整的AST的訪問方式,能夠讓你根據不同的情況更快速簡單的訪問具體的程式碼節點,譬如現在我們的檢測規則寫成了這個樣子:
1 2 3 4 5 6 7 |
root.walkDecls(node => { if (node.prop === 'text-transform' && node.value === 'uppercase') { report({ ... }); } }); |
我們也可以傳入一些基本的函式,譬如linear-gradient
,就像這個樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// disallow things like linear-gradient(top, blue, green) w. incorrect first valueroot.walkDecls(node => { const parsedValue = styleParser(node.value); parsedValue.walk(valueNode => { if (valueNode.type === 'function' && valueNode.value === 'linear-gradient') { const firstValueInGradient = styleParser.stringify(valueNode.nodes[0]); if (disallowedFirstValuesInGradient.indexOf(firstValueInGradient) > -1) { report({ ... }); } } }); }); |
這樣子寫出來的檢測規則可讀性更好,也更好去理解與維護,並且這種方式無論是在怎樣的CSS格式化的情況下,以及不管規則和宣告放在哪邊,都能正常地工作。
Custom rules:自定義規則
我們預設使用了一些Stylelint內建的規則,譬如declaration-no-important,selector-no-universal, 以及 selector-class-pattern。如何新增自定義規則的方法可以參考built-in plugin mechanism,而我們使用的譬如:
- slow-css-properties 來告警一些效能較差的屬性,譬如opacity或者box-shadow
- filters-with-svg-files 來告警Edge中不支援SVG的過濾
- use-variables來告警那些可以被內建的常量替換的一些變數
- common-properties-whitelist 來檢測是否有些誤寫的其實不存在的屬性
- mobile-flexbox 來檢測一些不被老版本手機瀏覽器支援的屬性
- text-transform-uppercase 來告警 “text-transform: uppercase”,這個在某些語言表現的不友好
我們也貢獻了部分規則 以及 ad外掛itions 給Stylelint。
Automatic replacement:自動替換
我們檢測過程中有一個重要的工作就是自動格式化,Linter會在發現某些問題的時候問你是否需要根據規則進行替換,這個功能會節約你大量的手動修改校正的時間。一般來說,我們提交程式碼之前都會審視下Linter報出的錯誤,然後去修復這些錯誤。可惜的是Stylelint並沒有內嵌的自動格式化與修復機制,因此我們重寫了部分的Stylelint的規則來增加一個自動替換與修復的功能。
Test all the things
我們老的Linter還有個問題就是沒有單元測試,這點就好像程式碼上線前不進行單元測試一樣不靠譜。我們面對的可能是任意格式的處理文字,因此我們也要保證我們的檢測規則能夠適用於真實有效的環境,這裡我們是選擇了Jest這個測試框架,Stylelint對它的支援挺好的,然後大概一個單元測試是這個樣子:
1 2 3 4 5 |
test.ok('div { background-image: linear-gradient( 0deg, blue, green 40%, red ); }', 'linear gradient with valid syntax'); test.notOk('a { background: linear-gradient(top, blue, green); }', message, 'linear-gradient with invalid syntax'); |
What‘s next
換一個靠譜的CSS Linter工具只是保證高質量的CSS的程式碼的第一步,我們還打算新增很多自定義的檢測規則來捕獲一些常見的錯誤,保證使用規定的最佳實踐以及統一程式碼約定規範。我們已經在JavaScript的校驗中進行了這一工作。
另外對於React社群中存在的CSS-in-JS這種寫法,對於CSS Linter也是個不小的挑戰,現在的大部分的Linter都是著眼於處理傳統的CSS檔案,以後會新增對於JSX的處理規範吧。