掃二維碼與項(xiàng)目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本文適用于所有前端開發(fā)人員。文章會介紹 PostCSS 的主功能實(shí)現(xiàn)原理,不是介紹 api,也不會介紹所有功能的原理,如果有需要了解全部功能或者查閱 API,可查看官方文檔:https://postcss.org/api/。

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價值的長期合作伙伴,公司提供的服務(wù)項(xiàng)目有:主機(jī)域名、網(wǎng)站空間、營銷軟件、網(wǎng)站建設(shè)、烏拉特后網(wǎng)站維護(hù)、網(wǎng)站推廣。
官網(wǎng)說:“PostCSS,一個使用 JavaScript 來處理CSS的框架”。這句話高度概括了 PostCSS 的作用,但是太抽象了。按我理解,PostCSS 主要做了三件事:
「如果沒有插件」,那么初始傳入的 CSS string 和 generate 生成的 CSS string 是一樣的。由此可見,PostCSS 本身并不處理任何具體的任務(wù),只有當(dāng)我們?yōu)槠涓郊痈鞣N插件之后,它才具有實(shí)用性。
下面分別詳細(xì)分析三個階段做的事。
CSS 規(guī)則集(rule-set)由選擇器和聲明塊組成:
五類對象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 對象用來描述 CSS 中的每一條聲明語句。
上邊 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)對象的實(shí)例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype 屬性
type: 'decl', // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 decl
value: "red" // value 字段記錄聲明的值
}
每個字段的含義和功能已經(jīng)以注釋的形式進(jìn)行了解釋。
Rule 對象是描述選擇器的。
上邊 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)對象的實(shí)例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
type: 'rule' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 rule
}
Root 對象是 AST 對象的根對象。
上邊 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)對象的實(shí)例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
type: 'root' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 root
}
CSS 中除了選擇器,還有一類語法是 @ 開頭的,例如 @import、@keyframes、@font-face,PostCSS 把這類語法解析成 AtRule 對象。
例如 @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)對象的實(shí)例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
type: 'atrule' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 atrule
}
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)對象的實(shí)例,如果不是,可以根據(jù)類型把對象的屬性設(shè)置為普通對象的 prototype
text: '你好', // text 記錄注釋內(nèi)容
type: 'comment' // type 記錄對象類型,是個枚舉值,聲明語句的 type 固定是 comment
}
從上一段可以知道,CSS 被解析成 Declaration、Rule、Root、AtRule、Comment 對象。這些對象有很多公共方法,PostCSS 用了面向?qū)ο蟮睦^承思想,把公共方法和公共屬性提取到了父類中。
Root、Rule、AtRule 都是可以有子節(jié)點(diǎn)的,都有 nodes 屬性,他們?nèi)齻€繼承自 Container 類,對 nodes 的操作方法都寫在 Container 類中。Container、Declaration、Comment 繼承自 Node 類,所有對象都有 Symbol('isClean')、Symbol('my')、raws、source、type 屬性,都有toString()、error()等方法,這些屬性和方法都定義在 Node 類中。
Container、Node 是用來提取公共屬性和方法,不會生成他們的實(shí)例。
五個類之間的繼承關(guān)系如下圖所示:
圖中沒有窮舉類的方法,好奇的同學(xué)可以看直接看源碼文件: https://github.com/postcss/postcss/tree/main/lib 。
算法對應(yīng)源碼中位置是:postcss/lib/parser.js中的parse方法,代碼量不大,可自行查看。
PostCSS 本身并不處理任何具體的任務(wù),只有當(dāng)我們?yōu)槠涓郊痈鞣N插件之后,它才具有實(shí)用性。
PostCSS 在把 CSS string 解析成 AST 對象后,會掃描一邊 AST 對象,每一種 AST 的對象都可以有對應(yīng)的監(jiān)聽器。在遍歷到某類型的對象時,如果有對象的監(jiān)聽器,就會執(zhí)行其監(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: (rootNode, helps) => {},
RootExit: (rootNode, helps) => {}
}
{
Rule: (ruleNode, helps) => {},
RuleExit: (ruleNode, 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)聽器。
除以特定屬性或者規(guī)則命名的事件監(jiān)聽器,PostCSS 還有以下四個:
{
postcssPlugin: string,
prepare: (result) => {},
Once: (root, helps) => {},
OnceExit: (root, helps) => {},
}
PostCSS 插件事件的整體執(zhí)行是:[prepare, Once, ...一類事件,OnceExit],postcssPlugin 是插件名稱,不是事件監(jiān)聽器。
{
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) => {}
}
此時再看市面上流行的基于 postcss 的工具,有沒有醍醐灌頂?
|
autoprefixer |
postcss-import-parser |
postcss-modules |
postcss-modules |
基于 postcss 的插件有很多,可查閱:https://github.com/postcss/postcss/blob/main/docs/plugins.md。
generate 的過程依舊是以深度優(yōu)先的方式遍歷 AST 對象,針對不同的實(shí)例對象進(jìn)行字符串的拼接。算法對應(yīng)源碼中位置是:postcss/lib/stringifier.js中的stringify方法,代碼量不大,可自行查看。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流