CSS TreeShaking原理揭祕:手寫 PurgeCss
TreeShaking 是通過靜態分析的方式找出原始碼中不會被使用的程式碼進行刪除,達到減小編譯打包產物的程式碼體積的目的。
JS 我們會用 Webpack、Terser 進行 Tree Shking,而 CSS 會用 PurgeCss。
PurgeCss 會分析 html 或其他程式碼中 css 選擇器的使用情況,進而刪除沒有被使用的 css。
是否對 PurgeCss 怎麼找到無用的 css 的原理比較好奇呢?今天我們就來手寫個簡易版 PurgeCss 來探究下吧。
思路分析
PurgeCss 要指定 css 應用到哪些 html,它會分析 html 中的 css 選擇器,根據分析結果來刪除沒有用到的 css:
const{PurgeCSS}=require('purgecss')
constpurgeCSSResult=awaitnewPurgeCSS().purge({
content:['**/*.html'],
css:['**/*.css']
})
我們要做的事情就可以分為兩部分:
- 提取 html 中的可能的 css 選擇器,包括 id、class、tag 等
- 分析 css 中的 rule,根據選擇器是否被 html 使用,刪掉沒被用到的部分奶茶加盟
從 html 中提取資訊的部分,叫做 html 提取器(extractor)。
我們可以基於 posthtml 來實現 html 的提取器,它可以做 html 的 parse、分析、轉換等,api 和 postcss 類似。
css 部分使用 postcss,通過 ast 可以分析出每一條 rule。
遍歷 css 的 rule,對每個 rule 的選擇器都判斷下是否在從 html 中提取到選擇器中,如果沒有,就代表沒有被使用,就刪掉該選擇器。
如果一個 rule 的所有的選擇器都刪掉了,那麼就把這個 rule 刪掉。
這就是 purgecss 的實現思路。我們來寫下程式碼。
程式碼實現
我們來寫一個 postcss 外掛來做這件事情,postcss 外掛就是基於 AST 做 css 的分析和轉換的。
constpurgePlugin=(options)=>{
return{
postcssPlugin:'postcss-purge',
Rule(rule){}
}
}
module.exports=purgePlugin;
postcss 外掛的形式是一個函式,接收外掛的配置引數,返回一個物件。物件裡宣告 Rule、AtRule、Decl 等的 listener,也就是對不同 AST 的處理函式。
這個 postcss 外掛的名字叫做 purge,可以被這樣呼叫:
constpostcss=require('postcss');
constpurge=require('./src/index');
constfs=require('fs');
constpath=require('path');
constcss=fs.readFileSync('./example/index.css');
postcss([purge({
html:path.resolve('./example/index.html'),
})]).process(css).then(result=>{
console.log(result.css);
});
通過引數傳入 html 的路徑,外掛裡可以通過 option.html 拿到。
接下來我們來實現下這個外掛。
前面分析過,實現過程整體分為兩步:
- 通過 posthtml 提取 html 中的 id、class、tag
- 遍歷 css 的 ast,刪掉沒被 html 使用的部分
我們封裝一個 htmlExtractor 來做提取的事情:
constpurgePlugin=(options)=>{
constextractInfo={
id:[],
class:[],
tag:[]
};
htmlExtractor(options&&options.html,extractInfo);
return{
postcssPlugin:'postcss-purge',
Rule(rule){}
}
}
module.exports=purgePlugin;
htmlExtractor 的具體實現就是讀取 html 的內容,對 html 做 parse 生成 AST,遍歷 AST,記錄 id、class、tag:
functionhtmlExtractor(html,extractInfo){
constcontent=fs.readFileSync(html,'utf-8');
constextractPlugin=options=>tree=>{
returntree.walk(node=>{
extractInfo.tag.push(node.tag);
if(node.attrs){
extractInfo.id.push(node.attrs.id)
extractInfo.class.push(node.attrs.class)
}
returnnode
});
}
posthtml([extractPlugin()]).process(content);
//過濾掉空值
extractInfo.id=extractInfo.id.filter(Boolean);
extractInfo.class=extractInfo.class.filter(Boolean);
extractInfo.tag=extractInfo.tag.filter(Boolean);
}
posthtml 的外掛形式和 postcss 類似,我們在 posthtml 外掛裡遍歷 AST 並記錄了一些資訊。
最後,過濾掉 id、class、tag 中的空值,就完成了提取。
我們先不著急做下一步,先來測試下現在的功能。
我們準備這樣一個 html:
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metahttp-equiv="X-UA-Compatible"content="IE=edge">
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<title>Document</title>
</head>
<body>
<divclass="aaa"></div>
<divid="ccc"></div>
<span></span>
</body>
</html>
測試下提取的資訊:
可以看到,id、class、tag 都正確的從 html 中提取了出來。
接下來,我們繼續做下一步:從 css 的 AST 中刪掉沒被使用的部分。
我們聲明瞭 Rule 的 listener,可以拿到 rule 的 AST。要分析的是 selector 部分,需要先根據 “,” 做拆分,然後對每一個選擇器做處理。
Rule(rule){
constnewSelector=rule.selector.split(',').map(item=>{
//對每個選擇器做轉換
}).filter(Boolean).join(',');
if(newSelector===''){
rule.remove();
}else{
rule.selector=newSelector;
}
}
選擇器可以用 postcss-selector-parser 來做 parse、分析和轉換。
處理以後的選擇器如果都被刪掉了,就說明這個 rule 的樣式就沒用了,就刪掉這個 rule。否則可能只是刪掉了部分選擇器,該樣式還會被用到。
constnewSelector=rule.selector.split(',').map(item=>{
consttransformed=selectorParser(transformSelector).processSync(item);
returntransformed!==item?'':item;
}).filter(Boolean).join(',');
if(newSelector===''){
rule.remove();
}else{
rule.selector=newSelector;
}
接下來實現對選擇器的分析和轉換,也就是 transformSelector 函式。
這部分的邏輯就是對每個選擇器判斷下是否在從 html 提取到的選擇器中,如果不在,就刪掉。
consttransformSelector=selectors=>{
selectors.walk(selector=>{
selector.nodes&&selector.nodes.forEach(selectorNode=>{
letshouldRemove=false;
switch(selectorNode.type){
case'tag':
if(extractInfo.tag.indexOf(selectorNode.value)==-1){
shouldRemove=true;
}
break;
case'class':
if(extractInfo.class.indexOf(selectorNode.value)==-1){
shouldRemove=true;
}
break;
case'id':
if(extractInfo.id.indexOf(selectorNode.value)==-1){
shouldRemove=true;
}
break;
}
if(shouldRemove){
selectorNode.remove();
}
});
});
};
我們完成了 html 中選擇器資訊的提取,和 css 根據 html 提取的資訊做無用 rule 的刪除,外掛的功能就已經完成了。
我們來測試下效果:
css:
.aaa,ee,ff{
color:red;
font-size:12px;
}
.bbb{
color:red;
font-size:12px;
}
#ccc{
color:red;
font-size:12px;
}
#ddd{
color:red;
font-size:12px;
}
p{
color:red;
font-size:12px;
}
span{
color:red;
font-size:12px;
}
html:
<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metahttp-equiv="X-UA-Compatible"content="IE=edge">
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<title>Document</title>
</head>
<body>
<divclass="aaa"></div>
<divid="ccc"></div>
<span></span>
</body>
</html>
按理說, p、#ddd、.bbb 的選擇器和樣式,ee、ff 的選擇器都會被刪除。
我們使用下該外掛:
constpostcss=require('postcss');
constpurge=require('./src/index');
constfs=require('fs');
constpath=require('path');
constcss=fs.readFileSync('./example/index.css');
postcss([purge({
html:path.resolve('./example/index.html'),
})]).process(css).then(result=>{
console.log(result.css);
});
經測試,功能是對的:
這就是 PurgeCss 的實現原理。我們完成了 css 的 three shaking!
當然,我們只是簡易版實現,有的地方做的不完善:
- 只實現了 html 提取器,而 PurgeCss 還有 jsx、pug、tsx 等提取器(不過思路都是一樣的)
- 只處理了單檔案,沒有處理多檔案(再加個迴圈就行)
- 只處理了 id、class、tag 選擇器,沒處理屬性選擇器(屬性選擇器的處理稍微複雜一些)
雖然沒有做到很完善,但是 PurgeCss 的實現思路已經通了,不是麼~
總結
JS 的 TreeShaking 使用 Webpack、Terser,而 CSS 的 TreeShaking 使用 PurgeCss。
我們實現了一個簡易版的 PurgeCss 來理清了它的實現原理:奶茶加盟
通過 html 提取器提取 html 中的選擇器資訊,然後對 CSS 的 AST 做過濾,根據 Rule 的 selector 是否被使用到來刪掉沒用到的 rule,達到 TreeShaking 的目的。
實現這個工具的過程中,我們學習了 postcss 和 posthtml 外掛的寫法,這兩者形式上很類似,只不過一個針對 css 做分析和轉換,一個針對 html。
Postcss 可以分析和轉換 CSS,比如這裡的刪除無用 css 就是一個很好的應用。你還見過別的 postcss 的很棒的應用場景麼,不妨一起來討論下吧~
本文來自部落格園,作者:Rlwyms,轉載請註明原文連結:www.seozatan.com