零基礎(chǔ)理解 PostCSS 的主流程
來(lái)自內(nèi)部 霍超群 同學(xué)的分享
本文適用于所有前端開發(fā)人員。文章會(huì)介紹 PostCSS 的主功能實(shí)現(xiàn)原理,不是介紹 api,也不會(huì)介紹所有功能的原理,如果有需要了解全部功能或者查閱 API,可查看官方文檔:https://postcss.org/api/。
什么是 PostCSS
官網(wǎng)說(shuō):“PostCSS,一個(gè)使用 JavaScript 來(lái)處理CSS的框架”。這句話高度概括了 PostCSS 的作用,但是太抽象了。按我理解,PostCSS 主要做了三件事:
parse:把 CSS 文件的字符串解析成抽象語(yǔ)法樹(Abstract Syntax Tree)的框架,解析過(guò)程中會(huì)檢查 CSS 語(yǔ)法是否正確,不正確會(huì)給出錯(cuò)誤提示。runPlugin: 執(zhí)行插件函數(shù)。PostCSS 本身不處理任何具體任務(wù),它提供了以特定屬性或者規(guī)則命名的事件。有特定功能的插件(如 autoprefixer、CSS Modules)會(huì)注冊(cè)事件監(jiān)聽器。PostCSS 會(huì)在這個(gè)階段,重新掃描 AST,執(zhí)行注冊(cè)的監(jiān)聽器函數(shù)。generate: 插件對(duì) AST 處理后,PostCSS 把處理過(guò)的 AST 對(duì)象轉(zhuǎn)成 CSS string。

「如果沒有插件」,那么初始傳入的 CSS string 和 generate 生成的 CSS string 是一樣的。由此可見,PostCSS 本身并不處理任何具體的任務(wù),只有當(dāng)我們?yōu)槠涓郊痈鞣N插件之后,它才具有實(shí)用性。
下面分別詳細(xì)分析三個(gè)階段做的事。
第一階段:parse
CSS 語(yǔ)法簡(jiǎn)述
CSS 規(guī)則集(rule-set)由選擇器和聲明塊組成:

- 選擇器指向您需要設(shè)置樣式的 HTML 元素。
- 聲明塊包含一條或多條用分號(hào)分隔的聲明。
- 每條聲明都包含一個(gè) CSS 屬性名稱和一個(gè)值,以冒號(hào)分隔。
- 多條 CSS 聲明用分號(hào)分隔,聲明塊用花括號(hào)括起來(lái)。
五類對(duì)象
AST 用五類對(duì)象描述 CSS 語(yǔ)法。這里舉個(gè)具體的例子,再打印出對(duì)應(yīng)的 AST 結(jié)果,對(duì)照了解 AST 五類對(duì)象和 CSS 語(yǔ)法的對(duì)應(yīng)關(guān)系。
app.css 文件中寫如下內(nèi)容:
@import?url('./app-02.css');
.container?{
??color:?red;
}
Declaration 對(duì)象
Declaration 對(duì)象用來(lái)描述 CSS 中的每一條聲明語(yǔ)句。
- type 標(biāo)記當(dāng)前對(duì)象的類型
- parent 記錄父對(duì)象的實(shí)例
- prop 記錄聲明中的屬性名
- value 記錄聲明中的值
- raws 字段記錄聲明前的字符串、聲明屬性和值之間的符號(hào)的字符串
- 其余字段解釋見代碼中的注釋。
上邊 CSS 文件中的color: red;會(huì)被描述成如下對(duì)象:
{
????parent:?Rule,???????//?外層的選擇器被轉(zhuǎn)譯成?Rule?對(duì)象,是當(dāng)前聲明對(duì)象的?parent
????prop:?"color",??????//?prop?字段記錄聲明的屬性
????raws:?{?????????????// raws 字段記錄聲明前、后的字符串,聲明屬性和值之間的字符串,以及前邊語(yǔ)句是否分號(hào)結(jié)束。
????????before:?'\n?',??//?raws.before?字段記錄聲明前的字符串
????????between:?':?',?//?raws.between?字段記錄聲明屬性和值之間的字符串
????},
????source:?{??????????//?source?字段記錄聲明語(yǔ)句的開始、結(jié)束位置,以及當(dāng)前文件的信息
????????start:?{?offset:?45,?column:?3,?line:?4?},
????????end:?{?offset:?55,?column:?13,?line:?4?},
????????input:?Input?{
????????????css:?'@import?url('./app-02.css');\n\n.container?{\n??color:?red;\n}',
????????????file:?'/Users/admin/temp/postcss/app.css',
????????????hasBOM:?false,
????????????Symbol(fromOffsetCache):?[0,?29,?30,?43,?57]
????????}
????},
????Symbol('isClean'):?false,??// Symbol(isClean)?字段默認(rèn)值都是 false,用于記錄當(dāng)前對(duì)象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會(huì)在后續(xù)解釋
????Symbol('my'):?true,????????//?Symbol(my)?字段默認(rèn)值都是?true,用于記錄當(dāng)前對(duì)象是否是對(duì)應(yīng)對(duì)象的實(shí)例,如果不是,可以根據(jù)類型把對(duì)象的屬性設(shè)置為普通對(duì)象的?prototype?屬性
????type:?'decl',????????????//?type?記錄對(duì)象類型,是個(gè)枚舉值,聲明語(yǔ)句的?type?固定是?decl
????value:?"red"?????????????//?value?字段記錄聲明的值
}
每個(gè)字段的含義和功能已經(jīng)以注釋的形式進(jìn)行了解釋。
Rule 對(duì)象
Rule 對(duì)象是描述選擇器的。
- type 記錄對(duì)象的類型
- parent 記錄父對(duì)象的實(shí)例
- nodes 記錄子對(duì)象的實(shí)例
- selector 記錄選擇器的字符串
- raws 記錄選擇器前的字符串、選擇器和大括號(hào)之間的字符串、最后一個(gè)聲明和結(jié)束大括號(hào)之間的字符串
- 其余字段解釋見代碼中的注釋。
上邊 app.css 文件中.container經(jīng)過(guò) postcss 轉(zhuǎn)譯后的對(duì)象是(每個(gè)字段的含義和功能已經(jīng)以注釋的形式進(jìn)行了解釋):
{
????nodes:?[Declaration],?//?nodes?記錄包含關(guān)系,Rule?對(duì)象包含?Declaration?對(duì)象
????parent:?Root,????????//?根對(duì)象是?Root?對(duì)象,是當(dāng)前聲明對(duì)象的?parent
????raws:?{??????????????//?raws?字段記錄如下
????????before:?'\n\n',??//?raws.before?字段記錄選擇器前的字符串
????????between:?'?',????//?raws.between?字段記錄選擇器和大括號(hào)之間的字符串
????????semicolon:?true,?//?raws.semicolon?字段記錄前置聲明語(yǔ)句是正常分號(hào)結(jié)束
????????after:?'\n'??????//?raws.after?字段記錄最后一個(gè)聲明和結(jié)束大括號(hào)之間的字符串
????},
????selector:'.container',?//?selector?記錄?selector
????source:?{????????????//?source?字段記錄選擇器語(yǔ)句的開始、結(jié)束位置,以及當(dāng)前文件的信息
????????start:?{?offset:?30,?column:?1,?line:?3?},
????????input:?Input?{
????????????css:?'@import?url('./app-02.css');\n\n.container?{\n??color:?red;\n}',
????????????file:?'/Users/admin/temp/postcss/app.css',
????????????hasBOM:?false,
????????????Symbol(fromOffsetCache):?[0,?29,?30,?43,?57]
????????},
????????end:?{?offset:?57,?column:?1,?line:?5?}
????},
????Symbol('isClean'):?false,??// Symbol(isClean)?字段默認(rèn)值都是 false,用于記錄當(dāng)前對(duì)象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會(huì)在后續(xù)解釋
????Symbol('my'):?true,????????//?Symbol(my)?字段默認(rèn)值都是?true,用于記錄當(dāng)前對(duì)象是否是對(duì)應(yīng)對(duì)象的實(shí)例,如果不是,可以根據(jù)類型把對(duì)象的屬性設(shè)置為普通對(duì)象的?prototype
????type:?'rule'???????????//?type?記錄對(duì)象類型,是個(gè)枚舉值,聲明語(yǔ)句的?type?固定是?rule
}
Root 對(duì)象
Root 對(duì)象是 AST 對(duì)象的根對(duì)象。
- type 記錄當(dāng)前對(duì)象的類型
- nodes 屬性記錄子節(jié)點(diǎn)對(duì)應(yīng)對(duì)象的實(shí)例。
上邊 app.css 文件中 root 對(duì)象是(每個(gè)字段的含義和功能已經(jīng)以注釋的形式進(jìn)行了解釋):
{
????nodes:?[AtRule,?Rule],?//?nodes?記錄子對(duì)象(選擇器和?@開頭的對(duì)象),AtRule?對(duì)象會(huì)在后邊提到
????raws:?{????????????????//?raws?字段記錄如下
????????semicolon:?false,??//?raws.semicolon?最后是否是分號(hào)結(jié)束
????????after:?''??????????//?raws.after?最后的空字符串
????},
????source:?{??????????????//?source?字段記錄根目錄語(yǔ)句的開始,以及當(dāng)前文件的信息
????????start:?{?offset:?0,?column:?1,?line:?1?},
????????input:?Input?{
????????????css:?'@import?url('./app-02.css');\n\n.container?{\n??color:?red;\n}',
????????????file:?'/Users/admin/temp/postcss/app.css',
????????????hasBOM:?false,
????????????Symbol(fromOffsetCache):?[0,?29,?30,?43,?57]
????????}
????},
????Symbol('isClean'):?false,??// Symbol(isClean)?字段默認(rèn)值都是 false,用于記錄當(dāng)前對(duì)象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會(huì)在后續(xù)解釋
????Symbol('my'):?true,????????//?Symbol(my)?字段默認(rèn)值都是?true,用于記錄當(dāng)前對(duì)象是否是對(duì)應(yīng)對(duì)象的實(shí)例,如果不是,可以根據(jù)類型把對(duì)象的屬性設(shè)置為普通對(duì)象的?prototype
????type:?'root'???????????//?type?記錄對(duì)象類型,是個(gè)枚舉值,聲明語(yǔ)句的?type?固定是?root
}
AtRule 對(duì)象
CSS 中除了選擇器,還有一類語(yǔ)法是 @ 開頭的,例如 @import、@keyframes、@font-face,PostCSS 把這類語(yǔ)法解析成 AtRule 對(duì)象。
- type 記錄當(dāng)前對(duì)象的類型
- parent 記錄當(dāng)前對(duì)象的父對(duì)象
- name 記錄
@緊跟著的單詞 - params 記錄 name 值
例如 @import url("./app-02.css"); 將被解析成如下對(duì)象:
{
????name:?"import",??????????????????//?name?記錄?@?緊跟著的單詞
????params:?"url('./app-02.css')",???//?params?記錄?name?值
????parent:?Root,????????????????????//?parent?記錄父對(duì)象
????raws:?{??????????????????????????//?raws?字段記錄如下
????????before:?'',??????????????????//?raws.before?記錄?@語(yǔ)句前的空字符串
????????between:?'',?????????????????//?raws.between?記錄?name?和?{?之間的空字符串
????????afterName:?'',????????????????//?raws.afterName?記錄?name?和?@?語(yǔ)句之間的空字符串
????????after:?'',???????????????????//?raws.after?記錄大括號(hào)和上一個(gè)?rule?之間的空字符串
????????semicolon:?false?????????????//?raws.semicolon?上一個(gè)規(guī)則是否是分號(hào)結(jié)束
????},
????source:?{????????????????????????//?source?字段記錄@語(yǔ)句的開始,以及當(dāng)前文件的信息
????????start:?{?offset:?0,?column:?1,?line:?1?},
????????end:?{?offset:?27,?column:?28,?line:?1?},
????????input:?Input?{
????????????css:?'@import?url('./app-02.css');\n\n.container?{\n??color:?red;\n}',
????????????file:?'/Users/admin/temp/postcss/app.css',
????????????hasBOM:?false,
????????????Symbol(fromOffsetCache):?[0,?29,?30,?43,?57]
????????}
????},
????Symbol('isClean'):?false,??// Symbol(isClean)?字段默認(rèn)值都是 false,用于記錄當(dāng)前對(duì)象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會(huì)在后續(xù)解釋
????Symbol('my'):?true,????????//?Symbol(my)?字段默認(rèn)值都是?true,用于記錄當(dāng)前對(duì)象是否是對(duì)應(yīng)對(duì)象的實(shí)例,如果不是,可以根據(jù)類型把對(duì)象的屬性設(shè)置為普通對(duì)象的?prototype
????type:?'atrule'??????????//?type?記錄對(duì)象類型,是個(gè)枚舉值,聲明語(yǔ)句的?type?固定是?atrule
}
Comment 對(duì)象
css 文件中的注釋被解析成 Comment 對(duì)象。text 字段記錄注釋內(nèi)容。/* 你好 */被解析成:
{
????parent:?Root,?????????????//?parent?記錄父對(duì)象
????raws:?{???????????????????//?raws?字段記錄如下
????????before:?'',???????????//?raws.before?記錄注釋語(yǔ)句前的空字符串
????????left:?'?',????????????//?raws.left?記錄注釋語(yǔ)句左側(cè)的空字符串
????????right:?'?'????????????//?raws.right?記錄注釋語(yǔ)句右側(cè)的空字符串
????},
????source:?{?????????????????//?source?字段記錄注釋語(yǔ)句的開始、結(jié)束位置,以及當(dāng)前文件的信息
????????start:?{…},?input:?Input,?end:?{…}
????},
????Symbol('isClean'):?false,??// Symbol(isClean)?字段默認(rèn)值都是 false,用于記錄當(dāng)前對(duì)象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會(huì)在后續(xù)解釋
????Symbol('my'):?true,????????//?Symbol(my)?字段默認(rèn)值都是?true,用于記錄當(dāng)前對(duì)象是否是對(duì)應(yīng)對(duì)象的實(shí)例,如果不是,可以根據(jù)類型把對(duì)象的屬性設(shè)置為普通對(duì)象的?prototype
????text:?'你好',?????????????//?text?記錄注釋內(nèi)容
????type:?'comment'??????????//?type?記錄對(duì)象類型,是個(gè)枚舉值,聲明語(yǔ)句的?type?固定是?comment
}
圖解五類對(duì)象之間的繼承關(guān)系
從上一段可以知道,CSS 被解析成 Declaration、Rule、Root、AtRule、Comment 對(duì)象。這些對(duì)象有很多公共方法,PostCSS 用了面向?qū)ο蟮睦^承思想,把公共方法和公共屬性提取到了父類中。
Root、Rule、AtRule 都是可以有子節(jié)點(diǎn)的,都有 nodes 屬性,他們?nèi)齻€(gè)繼承自 Container 類,對(duì) nodes 的操作方法都寫在 Container 類中。Container、Declaration、Comment 繼承自 Node 類,所有對(duì)象都有 Symbol('isClean')、Symbol('my')、raws、source、type 屬性,都有toString()、error()等方法,這些屬性和方法都定義在 Node 類中。
Container、Node 是用來(lái)提取公共屬性和方法,不會(huì)生成他們的實(shí)例。
五個(gè)類之間的繼承關(guān)系如下圖所示:

圖中沒有窮舉類的方法,好奇的同學(xué)可以看直接看源碼文件: https://github.com/postcss/postcss/tree/main/lib 。
把 CSS 語(yǔ)法解析成 AST 對(duì)象的具體算法
算法對(duì)應(yīng)源碼中位置是:postcss/lib/parser.js中的parse方法,代碼量不大,可自行查看。
第二階段:runPlugin
PostCSS 本身并不處理任何具體的任務(wù),只有當(dāng)我們?yōu)槠涓郊痈鞣N插件之后,它才具有實(shí)用性。
PostCSS 在把 CSS string 解析成 AST 對(duì)象后,會(huì)掃描一邊 AST 對(duì)象,每一種 AST 的對(duì)象都可以有對(duì)應(yīng)的監(jiān)聽器。在遍歷到某類型的對(duì)象時(shí),如果有對(duì)象的監(jiān)聽器,就會(huì)執(zhí)行其監(jiān)聽器。
第一類監(jiān)聽器
PostCSS 提供的「以特定屬性或者規(guī)則命名」的事件監(jiān)聽器,如下:
CHILDREAN 代表子節(jié)點(diǎn)的事件監(jiān)聽器。
//?root
['Root',?CHILDREN,?'RootExit']
//?AtRule
['AtRule',?'AtRule-import',?CHILDREN,?'AtRuleExit',?'AtRuleExit-import']
//?Rule
['Rule',?CHILDREN,?'RuleExit']
//?Declaration
['Declaration',?'Declaration-color',?'DeclarationExit',?'DeclarationExit-color']
//?Comment
['Comment',?'CommentExit']
PostCSS 以深度優(yōu)先的方式遍歷 AST 樹。
- 遍歷到 Root 根對(duì)象,第一步會(huì)執(zhí)行所有插件注冊(cè)的 Root 事件監(jiān)聽器,第二步檢查 Root 是否有子對(duì)象,如果有,則遍歷子對(duì)象,執(zhí)行子對(duì)象對(duì)應(yīng)的事件監(jiān)聽器;如果沒有子對(duì)象,則直接進(jìn)入第三步,第三步會(huì)執(zhí)行所有插件注冊(cè)的 RootExit 事件監(jiān)聽器。插件注冊(cè)的 Root、RootExit 事件的監(jiān)聽器只能是函數(shù)。函數(shù)的第一個(gè)參數(shù)是當(dāng)前訪問(wèn)的 AST 的 Root 對(duì)象,第二個(gè)參數(shù)是 postcss 的 Result 對(duì)象和一些其他屬性,通過(guò) Result 對(duì)象可以獲取 css string、opts 等信息。
{
??Root:?(rootNode,?helps)?=>?{},
??RootExit:?(rootNode,?helps)?=>?{}
}
- 遍歷到 Rule 對(duì)象,則和訪問(wèn) Root 根對(duì)象是一樣的邏輯,先執(zhí)行所有插件注冊(cè)的 Rule 事件監(jiān)聽器,再遍歷子對(duì)象,最后執(zhí)行所有插件注冊(cè)的 RuleExit 事件監(jiān)聽器。插件注冊(cè)的 Rule、RuleExit 事件的監(jiān)聽器只能是函數(shù)。
{
??Rule:?(ruleNode,?helps)?=>?{},
??RuleExit:?(ruleNode,?helps)?=>?{}
}
- 遍歷到 AtRule 對(duì)象。插件注冊(cè)的 AtRule 的事件監(jiān)聽器可以是函數(shù),也可以是對(duì)象。對(duì)象類型的監(jiān)聽器,對(duì)象屬性的 key 是 AtRule 對(duì)象的 name 值,value 是函數(shù)。AtRuleExit 是一樣的邏輯。事件的執(zhí)行順序是:
['AtRule', 'AtRule-import', CHILDREN, 'AtRuleExit', 'AtRuleExit-import']。CHILDREAN 代表子節(jié)點(diǎn)的事件。``` // 函數(shù) { AtRule: (atRuleNode, helps) => {} }
//?對(duì)象
{
??AtRule:?{
??????import:?(atRuleNode,?helps)?=>?{},
??????keyframes:?(atRuleNode,?helps)?=>?{}
??}
}
- 遍歷到 Declaration 對(duì)象。插件注冊(cè)的 Declaration 的事件監(jiān)聽器可以是函數(shù),也可以是對(duì)象,對(duì)象屬性的 key 是 Declaration 對(duì)象的 prop 值,value 是函數(shù)。DeclarationExitExit 是一樣的邏輯。事件的執(zhí)行順序是:
['Declaration', 'Declaration-color', 'DeclarationExit', 'DeclarationExit-color']。Declaration 沒有子對(duì)象,只需要執(zhí)行當(dāng)前對(duì)象的事件,不需要深度執(zhí)行子對(duì)象的事件。
//?函數(shù)
{
??Declaration:?(declarationNode,?helps)?=>?{}
}
//?對(duì)象
{
??Declaration:?{
??????color:?(declarationNode,?helps)?=>?{},
??????border:?(declarationNode,?helps)?=>?{}
??}
}
- 遍歷到 Comment 對(duì)象。依次執(zhí)行所有插件注冊(cè)的 Comment 事件監(jiān)聽器,再執(zhí)行所有插件注冊(cè)的 CommentExit 事件監(jiān)聽器。
第二類監(jiān)聽器
除以特定屬性或者規(guī)則命名的事件監(jiān)聽器,PostCSS 還有以下四個(gè):
{
??postcssPlugin:?string,
??prepare:?(result)?=>?{},
??Once:?(root,?helps)?=>?{},
??OnceExit:?(root,?helps)?=>?{},
}
PostCSS 插件事件的整體執(zhí)行是:[prepare, Once, ...一類事件,OnceExit],postcssPlugin 是插件名稱,不是事件監(jiān)聽器。
- postcssPlugin:字符串類型,插件的名字,在插件執(zhí)行報(bào)錯(cuò),提示用戶是哪個(gè)插件報(bào)錯(cuò)了。
- prepare:函數(shù)類型,prepare 是最先執(zhí)行的,在所有事件執(zhí)行前執(zhí)行的,插件多個(gè)監(jiān)聽器間共享數(shù)據(jù)時(shí)使用。prepare 的入?yún)⑹?Result 對(duì)象,返回值是監(jiān)聽器對(duì)象,通過(guò) Result 對(duì)象可以獲取 css string、opts 等信息。
{
??postcssPlugin:?"PLUGIN?NAME",
??prepare(result)?{
????const?variables?=?{};
????return?{
??????Declaration(node)?{
????????if?(node.variable)?{
??????????variables[node.prop]?=?node.value;
????????}
??????},
??????OnceExit()?{
????????console.log(variables);
??????},
????};
??},
};
- Once:函數(shù)類型,在 prepare 后,一類事件前執(zhí)行,Once 只會(huì)執(zhí)行一次。
{
???Once:?(root,?helps)?=>?{}
}
- OnceExit: 函數(shù)類型,在一類事件后執(zhí)行,OnceExit 只會(huì)執(zhí)行一次。
插件源碼截圖
此時(shí)再看市面上流行的基于 postcss 的工具,有沒有醍醐灌頂?
| autoprefixer | postcss-import-parser | postcss-modules | postcss-modules |
|---|---|---|---|
![]() | ![]() | ![]() ![]() | ![]() ![]() |
插件有哪些?
基于 postcss 的插件有很多,可查閱:https://github.com/postcss/postcss/blob/main/docs/plugins.md。
第三階段:generate
generate 的過(guò)程依舊是以深度優(yōu)先的方式遍歷 AST 對(duì)象,針對(duì)不同的實(shí)例對(duì)象進(jìn)行字符串的拼接。算法對(duì)應(yīng)源碼中位置是:postcss/lib/stringifier.js中的stringify方法,代碼量不大,可自行查看。






