這篇文章是我在公司內部的一個分享,大部分時間都在調試 postcss 源碼,即 postcss 將 css 字符串 解析為 CSS AST 的過程,很可惜這部分是不可見的,我打算錄制視頻放到B站上面,后續再更新。
本文目標:
- 掌握 postcss 的使用
- 自定義 postcss 插件
- 掌握 stylelint 的使用
- 自定義 stylelint rule
- 擴展 css parser 解釋器
1. postcss 是什么
在聊 postcss 之前,我們需要知道什么是 CSS 后處理工具。我們比較熟悉的 Less/Sass/Stylus,這類工具都屬于CSS 預處理工具。預處理指的是通過特殊的規則,將非 css 文本格式最終生成 css 文件,而 postcss 則是對 CSS 進行處理,最終生成CSS。
可能大部分前端開發者都使用過 Autoprefixer 這款插件,它以 Can I Use (瀏覽器兼容性支持) 為基礎,自動處理兼容性問題,下面是一個簡單的例子:
// Autoprefixer 處理前的CSS樣式
.container {
display: flex;
}
.item {
flex: 1;
}
// Autoprefixer 處理后的CSS樣式
.container {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.item {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
在這個例子中過,通過使用 Autoprefixer 插件,幫助我們自動處理瀏覽器前綴,極大的提高了編碼效率。其實,Autoprefixer 正是 postcss 眾多插件中的一款,postcss 提供的簡潔明了API,并且文檔十分詳細,這為其生態建設提供了有力的支撐。點擊 這里 查看更多可用插件。
2. postcss 如何使用
兩個主要的功能:
- 轉換 css,這是我們最常使用的
- 獲取 css ast,當我們編寫插件時需要掌握
2.1 轉換 css
這里只介紹 postcss API,如果您使用 webpack,只需要將 postcss 包裝為 postcss-loader 即可。
// 01-simple-demo
const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const precss = require('precss')
const fs = require('fs')
const path = require('path')
const src = path.resolve('./src/app.css');
const dest = path.resolve('./dest/app.css');
fs.readFile(src, (err, css) => {
postcss([precss, autoprefixer]) // [precss, autoprefixer] 為使用的插件列表,返回 Processor 對象
.process(css) // process 接收 css 資源
.then(result => { // result 為 Result 實例
fs.writeFile(dest, result.css, function(err) {
if (err) throw err;
});
})
})
相關源碼:
- postcss/lib/postcss.js
- postcss/lib/processor.js
2.2 獲取 CSS AST
// 02-use-parser
const postcss = require('postcss')
const fs = require('fs')
const path = require('path')
const src = path.resolve('./src/app.css');
const css = fs.readFileSync(src);
const root = postcss.parse(css);
console.log(root); // CSS 抽象語法樹
相關源碼:
- postcss/lib/parse.js
- postcss/lib/parser.js
3. postcss 的運行過程
主要步驟:
- 解釋:接收輸入的css,將css內容處理成css抽象語法樹。
- 轉換:根據配置插件的順序對樹型結構的 AST 進行操作。
- 輸出:最終將處理后獲得的 AST S 對象輸出為 css 文件。
主要內容:
- 標記器:將 css 拆解為 token 序列,為語法樹提供基礎(postcss/lib/tokenize.js ...)
- 解釋器:通過語法分享,將 token 序列轉換為語法樹(postcss/lib/parser.js ...)
- 處理器:根據插件配置,對語法樹做一些轉換操作(postcss/lib/lazy-result.js ...)
最不容易理解的也是最難的點:CSS AST 的生成以及操作。
4. postcss CSS AST
你暫且將 AST 理解一個節點樹,這些節點不完全相同,它們繼承自同一個節點(源碼中為Container)。
在學習 postcss 初期,通過查看可視化的 postcss css 語法樹,可以幫助你理解。使用使用 css ast 在線工具,下圖為一個很標準的 css 文檔,有注釋、媒體查詢,以及選擇器樣式:
/**
* Paste or drop some CSS here and explore
* the syntax tree created by chosen parser.
* Enjoy!
*/
@media screen and (min-width: 480px) {
body {
background-color: lightgreen;
}
}
#main {
border: 1px solid black;
}
ul li {
padding: 5px;
}
上面的 css 最終會處理為下圖結構,通過打印信息我們可以發現樹型結構的 JS 對象是一個名為 Root 的構造函數,而起樹型結構的 nodes 節點下還有 Common,AtRule, Rule 構造函數。

CSS AST 節點主要有以下構造類組成:
- Root: 根結點,整個處理過程基本上都在圍繞著 Root,Commont,AtRule,Rule 都是它的子節點。
- Commont: css 中的注釋信息,注釋的內容在 comment.text 下。
- AtRule: 帶@標識的部分,name 為標識名稱,params 為標識參數。nodes 為內部包含的其他子節點,可以是 Commont,AtRule,Rule,這讓我們可以自定義更多的規則。
- Declaration:每個 css 屬性以及屬性值就代表一個 declaration
4.1 Rule 選擇器節點
一個選擇器代表一個Rule,選擇器對應的樣式列表 nodes 為 Declaration構造函數

- raws
- before 距離前一個兄弟節點之間的內容
- between 選擇器與 { 之間的內容
- semicolon 最后一個屬性是否帶分號
- after 最后一個屬性 和 } 之間的內容

- type 節點類型
- nodes 子節點
- source
- start 開始位置
- end 結束位置
- selecter 選擇器
大部分節點結構是類似的,如果你理解了 Rule 節點的結構,相信其他類型的節點對你也是很輕松的!
4.2 Declaration 屬性節點
Declaration 是 css 樣式屬性,prop為樣式屬性,value為樣式值。可給 Rule 手動添加樣式屬性,也可以修改prop,value。上文提到的 Autoprefixer 就是通過 clone 當前屬性,修改 prop 并添加到選擇器下,Declaration 節點非常簡單:

4.3 Comment 注釋節點

5. 各構造器方法和屬性
大部分節點都繼承了 Container,因此我們先看看公共屬性:
- nodes 子節點
- parent 父節點
- raws 相當于分隔符集合
- source 位置范圍
- type 節點類型
- last 該節點的子節點的最后一個
- first 該節點的子節點的第一個
- after() 在當前節點的后面插入一個節點,等價
node.parent.insertAfter(node, add)
- cleanRaws 代碼格式化(保持縮進...)
- clone 節點克隆
- cloneBefore
- cloneAfter
- each 遍歷兒子節點
- error 拋出一個錯誤
- every 條件遍歷
- index 獲取節點在父節點中的索引
- insertAfter
- insertBefore
- next
- positionInside Convert string index to line/column
- prepend
- push
- raw()
- remove()
- removeAll() Removes all children from the container and cleans their parent properties.
- removeChild
- replaceValues 用于遍歷所有子孫節點 decl value
- root()
- some() Returns true if callback returns true for (at least) one of the container’s children.some()
- toJSON() 打印 JSON,使用 JSON.stringify() 有循環依賴
- toString() 獲取轉換后的 css
- walk() 遍歷所有子孫節點,這個接口非常有用哦
- walkAtRules 遍歷艾特節點
- walkComments 遍歷注釋節點
- walkRules 遍歷選擇器節點
- walkDecls 遍歷屬性節點
5.1 Rule
- 公共屬性方法
- selector 選擇器
- selectors 選擇器數組
[圖片上傳失敗...(image-ed56fe-1630568107685)]
5.2 Declaration
- 公共屬性方法
- prop
- value
5.3 Comment
- 公共屬性方法
- text
6. 如何使用 postcss 插件
插件用于豐富 postcss 的功能。
插件編寫文檔
postcss([PluginA({}), PluginB({})])
.process(css, {from: ``, to: ``})
.then(result => {
// do ...
}).catch(error => {
throw new Error(error)
})
7. 如何編寫一個 postcss 插件
上文中,我們對 css 處理后生成的 Root 以及其節點下的 Commont,AtRule,Rule, Declaration 有了基本的認識,那么我們是如何獲得Root,又將拿這些構造函數做些什么呢。
7.1 案例:css選擇器深度校驗
const fs = require('fs')
const path = require('path')
const postcss = require('postcss')
const css = fs.readFileSync(path.resolve(__dirname, './main.css'), 'utf8')
const checkDepth = postcss.plugin('check-depth', (opt) => {
opt = opt || { depth: 3 }
return root => {
root.walkRules(rule => {
let selector = rule.selector.replace(/(^\s*)|(\s*$)/g, '')
if (selector.split(/\s/).length > opt.depth) {
throw rule.error(`css selector depth is too long`)
}
})
}
})
postcss([checkDepth({
depth: 3
})])
.process(css, { from: ``, to: `` })
.then(result => {
console.log(result);
})
.catch(error => {
throw new Error(error)
})
7.2 案例:px 轉 rem
const postcss = require('postcss') // postcss
module.exports = postcss.plugin('px2rem', function(opts) {
opts = opts || {};
return function (root, result) {
root.replaceValues(/\d+px/, { fast: 'px' }, string => {
return opts.ratio * parseInt(string) + 'rem'
})
}
});
通過上述插件代碼的示例,可以看出整個流程還是很清晰的
- 重點對象:Root,Commont,AtRule,Rule, Declaration,Result;
- 遍歷方法:walkCommonts,walkAtRules,walkRules,walkDels
8. postcss 的運用
- stylelint 的使用
- 擴展 stylelint 規則
- ...見代碼