新聞中心
本文適用于所有前端開發(fā)人員。文章會介紹 PostCSS 的主功能實現(xiàn)原理,不是介紹 api,也不會介紹所有功能的原理,如果有需要了解全部功能或者查閱 API,可查看官方文檔:https://postcss.org/api/。

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價值的長期合作伙伴,公司提供的服務(wù)項目有:主機(jī)域名、網(wǎng)站空間、營銷軟件、網(wǎng)站建設(shè)、烏拉特后網(wǎng)站維護(hù)、網(wǎng)站推廣。
什么是 PostCSS
官網(wǎng)說:“PostCSS,一個使用 JavaScript 來處理CSS的框架”。這句話高度概括了 PostCSS 的作用,但是太抽象了。按我理解,PostCSS 主要做了三件事:
- parse:把 CSS 文件的字符串解析成抽象語法樹(Abstract Syntax Tree)的框架,解析過程中會檢查 CSS 語法是否正確,不正確會給出錯誤提示。
- runPlugin: 執(zhí)行插件函數(shù)。PostCSS 本身不處理任何具體任務(wù),它提供了以特定屬性或者規(guī)則命名的事件。有特定功能的插件(如 autoprefixer、CSS Modules)會注冊事件監(jiān)聽器。PostCSS 會在這個階段,重新掃描 AST,執(zhí)行注冊的監(jiān)聽器函數(shù)。
- generate: 插件對 AST 處理后,PostCSS 把處理過的 AST 對象轉(zhuǎn)成 CSS string。
「如果沒有插件」,那么初始傳入的 CSS string 和 generate 生成的 CSS string 是一樣的。由此可見,PostCSS 本身并不處理任何具體的任務(wù),只有當(dāng)我們?yōu)槠涓郊痈鞣N插件之后,它才具有實用性。
下面分別詳細(xì)分析三個階段做的事。
第一階段:parse
CSS 語法簡述
CSS 規(guī)則集(rule-set)由選擇器和聲明塊組成:
- 選擇器指向您需要設(shè)置樣式的 HTML 元素。
- 聲明塊包含一條或多條用分號分隔的聲明。
- 每條聲明都包含一個 CSS 屬性名稱和一個值,以冒號分隔。
- 多條 CSS 聲明用分號分隔,聲明塊用花括號括起來。
五類對象AST 用五類對象描述 CSS 語法。這里舉個具體的例子,再打印出對應(yīng)的 AST 結(jié)果,對照了解 AST 五類對象和 CSS 語法的對應(yīng)關(guān)系。
app.css 文件中寫如下內(nèi)容:
@import url('./app-02.css');
.container {
color: red;
}
Declaration 對象
Declaration 對象用來描述 CSS 中的每一條聲明語句。
- type 標(biāo)記當(dāng)前對象的類型
- parent 記錄父對象的實例
- prop 記錄聲明中的屬性名
- value 記錄聲明中的值
- raws 字段記錄聲明前的字符串、聲明屬性和值之間的符號的字符串
- 其余字段解釋見代碼中的注釋。
上邊 CSS 文件中的color: red;會被描述成如下對象:
{
parent: Rule, // 外層的選擇器被轉(zhuǎn)譯成 Rule 對象,是當(dāng)前聲明對象的 parent
prop: "color", // prop 字段記錄聲明的屬性
raws: { // raws 字段記錄聲明前、后的字符串,聲明屬性和值之間的字符串,以及前邊語句是否分號結(jié)束。
before: '\n ', // raws.before 字段記錄聲明前的字符串
between: ': ', // raws.between 字段記錄聲明屬性和值之間的字符串
},
source: { // source 字段記錄聲明語句的開始、結(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)前對象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會在后續(xù)解釋
Symbol('my'): true, // Symbol(my) 字段默認(rèn)值都是 true,用于記錄當(dāng)前對象是否是對應(yīng)對象的實例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype 屬性
type: 'decl', // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 decl
value: "red" // value 字段記錄聲明的值
}
每個字段的含義和功能已經(jīng)以注釋的形式進(jìn)行了解釋。
Rule 對象
Rule 對象是描述選擇器的。
- type 記錄對象的類型
- parent 記錄父對象的實例
- nodes 記錄子對象的實例
- selector 記錄選擇器的字符串
- raws 記錄選擇器前的字符串、選擇器和大括號之間的字符串、最后一個聲明和結(jié)束大括號之間的字符串
- 其余字段解釋見代碼中的注釋。
上邊 app.css 文件中.container經(jīng)過 postcss 轉(zhuǎn)譯后的對象是(每個字段的含義和功能已經(jīng)以注釋的形式進(jìn)行了解釋):
{
nodes: [Declaration], // nodes 記錄包含關(guān)系,Rule 對象包含 Declaration 對象
parent: Root, // 根對象是 Root 對象,是當(dāng)前聲明對象的 parent
raws: { // raws 字段記錄如下
before: '\n\n', // raws.before 字段記錄選擇器前的字符串
between: ' ', // raws.between 字段記錄選擇器和大括號之間的字符串
semicolon: true, // raws.semicolon 字段記錄前置聲明語句是正常分號結(jié)束
after: '\n' // raws.after 字段記錄最后一個聲明和結(jié)束大括號之間的字符串
},
selector:'.container', // selector 記錄 selector
source: { // source 字段記錄選擇器語句的開始、結(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)前對象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會在后續(xù)解釋
Symbol('my'): true, // Symbol(my) 字段默認(rèn)值都是 true,用于記錄當(dāng)前對象是否是對應(yīng)對象的實例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
type: 'rule' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 rule
}
Root 對象
Root 對象是 AST 對象的根對象。
- type 記錄當(dāng)前對象的類型
- nodes 屬性記錄子節(jié)點對應(yīng)對象的實例。
上邊 app.css 文件中 root 對象是(每個字段的含義和功能已經(jīng)以注釋的形式進(jìn)行了解釋):
{
nodes: [AtRule, Rule], // nodes 記錄子對象(選擇器和 @開頭的對象),AtRule 對象會在后邊提到
raws: { // raws 字段記錄如下
semicolon: false, // raws.semicolon 最后是否是分號結(jié)束
after: '' // raws.after 最后的空字符串
},
source: { // source 字段記錄根目錄語句的開始,以及當(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)前對象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會在后續(xù)解釋
Symbol('my'): true, // Symbol(my) 字段默認(rèn)值都是 true,用于記錄當(dāng)前對象是否是對應(yīng)對象的實例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
type: 'root' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 root
}
AtRule 對象
CSS 中除了選擇器,還有一類語法是 @ 開頭的,例如 @import、@keyframes、@font-face,PostCSS 把這類語法解析成 AtRule 對象。
- type 記錄當(dāng)前對象的類型
- parent 記錄當(dāng)前對象的父對象
- name 記錄@緊跟著的單詞
- params 記錄 name 值
例如 @import url("./app-02.css"); 將被解析成如下對象:
{
name: "import", // name 記錄 @ 緊跟著的單詞
params: "url('./app-02.css')", // params 記錄 name 值
parent: Root, // parent 記錄父對象
raws: { // raws 字段記錄如下
before: '', // raws.before 記錄 @語句前的空字符串
between: '', // raws.between 記錄 name 和 { 之間的空字符串
afterName: '', // raws.afterName 記錄 name 和 @ 語句之間的空字符串
after: '', // raws.after 記錄大括號和上一個 rule 之間的空字符串
semicolon: false // raws.semicolon 上一個規(guī)則是否是分號結(jié)束
},
source: { // source 字段記錄@語句的開始,以及當(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)前對象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會在后續(xù)解釋
Symbol('my'): true, // Symbol(my) 字段默認(rèn)值都是 true,用于記錄當(dāng)前對象是否是對應(yīng)對象的實例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
type: 'atrule' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 atrule
}
Comment 對象
css 文件中的注釋被解析成 Comment 對象。text 字段記錄注釋內(nèi)容。/* 你好 */被解析成:
{
parent: Root, // parent 記錄父對象
raws: { // raws 字段記錄如下
before: '', // raws.before 記錄注釋語句前的空字符串
left: ' ', // raws.left 記錄注釋語句左側(cè)的空字符串
right: ' ' // raws.right 記錄注釋語句右側(cè)的空字符串
},
source: { // source 字段記錄注釋語句的開始、結(jié)束位置,以及當(dāng)前文件的信息
start: {…}, input: Input, end: {…}
},
Symbol('isClean'): false, // Symbol(isClean) 字段默認(rèn)值都是 false,用于記錄當(dāng)前對象關(guān)聯(lián)的 plugin 是否執(zhí)行。plugin 會在后續(xù)解釋
Symbol('my'): true, // Symbol(my) 字段默認(rèn)值都是 true,用于記錄當(dāng)前對象是否是對應(yīng)對象的實例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
text: '你好', // text 記錄注釋內(nèi)容
type: 'comment' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 comment
}
圖解五類對象之間的繼承關(guān)系
從上一段可以知道,CSS 被解析成 Declaration、Rule、Root、AtRule、Comment 對象。這些對象有很多公共方法,PostCSS 用了面向?qū)ο蟮睦^承思想,把公共方法和公共屬性提取到了父類中。
Root、Rule、AtRule 都是可以有子節(jié)點的,都有 nodes 屬性,他們?nèi)齻€繼承自 Container 類,對 nodes 的操作方法都寫在 Container 類中。Container、Declaration、Comment 繼承自 Node 類,所有對象都有 Symbol('isClean')、Symbol('my')、raws、source、type 屬性,都有toString()、error()等方法,這些屬性和方法都定義在 Node 類中。
Container、Node 是用來提取公共屬性和方法,不會生成他們的實例。
五個類之間的繼承關(guān)系如下圖所示:
圖中沒有窮舉類的方法,好奇的同學(xué)可以看直接看源碼文件: https://github.com/postcss/postcss/tree/main/lib 。
把 CSS 語法解析成 AST 對象的具體算法
算法對應(yīng)源碼中位置是:postcss/lib/parser.js中的parse方法,代碼量不大,可自行查看。
第二階段:runPlugin
PostCSS 本身并不處理任何具體的任務(wù),只有當(dāng)我們?yōu)槠涓郊痈鞣N插件之后,它才具有實用性。
PostCSS 在把 CSS string 解析成 AST 對象后,會掃描一邊 AST 對象,每一種 AST 的對象都可以有對應(yīng)的監(jiān)聽器。在遍歷到某類型的對象時,如果有對象的監(jiān)聽器,就會執(zhí)行其監(jiān)聽器。
第一類監(jiān)聽器
PostCSS 提供的「以特定屬性或者規(guī)則命名」的事件監(jiān)聽器,如下:
CHILDREAN 代表子節(jié)點的事件監(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 根對象,第一步會執(zhí)行所有插件注冊的 Root 事件監(jiān)聽器,第二步檢查 Root 是否有子對象,如果有,則遍歷子對象,執(zhí)行子對象對應(yīng)的事件監(jiān)聽器;如果沒有子對象,則直接進(jìn)入第三步,第三步會執(zhí)行所有插件注冊的 RootExit 事件監(jiān)聽器。插件注冊的 Root、RootExit 事件的監(jiān)聽器只能是函數(shù)。函數(shù)的第一個參數(shù)是當(dāng)前訪問的 AST 的 Root 對象,第二個參數(shù)是 postcss 的 Result 對象和一些其他屬性,通過 Result 對象可以獲取 css string、opts 等信息。
{
Root: (rootNode, helps) => {},
RootExit: (rootNode, helps) => {}
}
- 遍歷到 Rule 對象,則和訪問 Root 根對象是一樣的邏輯,先執(zhí)行所有插件注冊的 Rule 事件監(jiān)聽器,再遍歷子對象,最后執(zhí)行所有插件注冊的 RuleExit 事件監(jiān)聽器。插件注冊的 Rule、RuleExit 事件的監(jiān)聽器只能是函數(shù)。
{
Rule: (ruleNode, helps) => {},
RuleExit: (ruleNode, helps) => {}
}
- 遍歷到 AtRule 對象。插件注冊的 AtRule 的事件監(jiān)聽器可以是函數(shù),也可以是對象。對象類型的監(jiān)聽器,對象屬性的 key 是 AtRule 對象的 name 值,value 是函數(shù)。AtRuleExit 是一樣的邏輯。事件的執(zhí)行順序是:['AtRule', 'AtRule-import', CHILDREN, 'AtRuleExit', 'AtRuleExit-import']。CHILDREAN 代表子節(jié)點的事件。``` // 函數(shù) { AtRule: (atRuleNode, helps) => {} }
// 對象
{
AtRule: {
import: (atRuleNode, helps) => {},
keyframes: (atRuleNode, helps) => {}
}
}
遍歷到 Declaration 對象。插件注冊的 Declaration 的事件監(jiān)聽器可以是函數(shù),也可以是對象,對象屬性的 key 是 Declaration 對象的 prop 值,value 是函數(shù)。DeclarationExitExit 是一樣的邏輯。事件的執(zhí)行順序是:['Declaration', 'Declaration-color', 'DeclarationExit', 'DeclarationExit-color']。Declaration 沒有子對象,只需要執(zhí)行當(dāng)前對象的事件,不需要深度執(zhí)行子對象的事件。
// 函數(shù)
{
Declaration: (declarationNode, helps) => {}
}
// 對象
{
Declaration: {
color: (declarationNode, helps) => {},
border: (declarationNode, helps) => {}
}
}
遍歷到 Comment 對象。依次執(zhí)行所有插件注冊的 Comment 事件監(jiān)聽器,再執(zhí)行所有插件注冊的 CommentExit 事件監(jiān)聽器。
第二類監(jiān)聽器
除以特定屬性或者規(guī)則命名的事件監(jiān)聽器,PostCSS 還有以下四個:
{
postcssPlugin: string,
prepare: (result) => {},
Once: (root, helps) => {},
OnceExit: (root, helps) => {},
}
PostCSS 插件事件的整體執(zhí)行是:[prepare, Once, ...一類事件,OnceExit],postcssPlugin 是插件名稱,不是事件監(jiān)聽器。
- postcssPlugin:字符串類型,插件的名字,在插件執(zhí)行報錯,提示用戶是哪個插件報錯了。
- prepare:函數(shù)類型,prepare 是最先執(zhí)行的,在所有事件執(zhí)行前執(zhí)行的,插件多個監(jiān)聽器間共享數(shù)據(jù)時使用。prepare 的入?yún)⑹?Result 對象,返回值是監(jiān)聽器對象,通過 Result 對象可以獲取 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 只會執(zhí)行一次。
{
Once: (root, helps) => {}
}
- OnceExit: 函數(shù)類型,在一類事件后執(zhí)行,OnceExit 只會執(zhí)行一次。
插件源碼截圖
此時再看市面上流行的基于 postcss 的工具,有沒有醍醐灌頂?
|
autoprefixer |
postcss-import-parser |
postcss-modules |
postcss-modules |
插件有哪些?
基于 postcss 的插件有很多,可查閱:https://github.com/postcss/postcss/blob/main/docs/plugins.md。
第三階段:generate
generate 的過程依舊是以深度優(yōu)先的方式遍歷 AST 對象,針對不同的實例對象進(jìn)行字符串的拼接。算法對應(yīng)源碼中位置是:postcss/lib/stringifier.js中的stringify方法,代碼量不大,可自行查看。
本文題目:零基礎(chǔ)理解PostCSS的主流程
分享路徑:http://fisionsoft.com.cn/article/dheogio.html


咨詢
建站咨詢
