自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

CSS TreeShking 原理揭秘: 手寫 PurgeCss

開發(fā) 前端
TreeShking 是通過靜態(tài)分析的方式找出源碼中不會被使用的代碼進行刪除,達到減小編譯打包產物的代碼體積的目的。

[[439953]]

TreeShking 是通過靜態(tài)分析的方式找出源碼中不會被使用的代碼進行刪除,達到減小編譯打包產物的代碼體積的目的。

JS 我們會用 Webpack、Terser 進行 Tree Shking,而 CSS 會用 PurgeCss。

PurgeCss 會分析 html 或其他代碼中 css 選擇器的使用情況,進而刪除沒有被使用的 css。

是否對 PurgeCss 怎么找到無用的 css 的原理比較好奇呢?今天我們就來手寫個簡易版 PurgeCss 來探究下吧。

思路分析

PurgeCss 要指定 css 應用到哪些 html,它會分析 html 中的 css 選擇器,根據分析結果來刪除沒有用到的 css:

  1. const { PurgeCSS } = require('purgecss'
  2. const purgeCSSResult = await new PurgeCSS().purge({ 
  3.   content: ['**/*.html'], 
  4.   css: ['**/*.css'
  5. }) 

我們要做的事情就可以分為兩部分:

  • 提取 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 的分析和轉換的。

  1. const purgePlugin = (options) => { 
  2.    
  3.     return { 
  4.         postcssPlugin: 'postcss-purge'
  5.         Rule (rule) {} 
  6.     } 
  7.  
  8. module.exports = purgePlugin; 

postcss 插件的形式是一個函數,接收插件的配置參數,返回一個對象。對象里聲明 Rule、AtRule、Decl 等的 listener,也就是對不同 AST 的處理函數。

這個 postcss 插件的名字叫做 purge,可以被這樣調用:

  1. const postcss = require('postcss'); 
  2. const purge = require('./src/index'); 
  3. const fs = require('fs'); 
  4. const path = require('path'); 
  5. const css = fs.readFileSync('./example/index.css'); 
  6.  
  7. postcss([purge({ 
  8.     html: path.resolve('./example/index.html'), 
  9. })]).process(css).then(result => { 
  10.     console.log(result.css); 
  11. }); 

通過參數傳入 html 的路徑,插件里可以通過 option.html 拿到。

接下來我們來實現下這個插件。

前面分析過,實現過程整體分為兩步:

  • 通過 posthtml 提取 html 中的 id、class、tag
  • 遍歷 css 的 ast,刪掉沒被 html 使用的部分

我們封裝一個 htmlExtractor 來做提取的事情:

  1. const purgePlugin = (options) => { 
  2.     const extractInfo = { 
  3.         id: [], 
  4.         class: [], 
  5.         tag: [] 
  6.     }; 
  7.  
  8.     htmlExtractor(options && options.html, extractInfo); 
  9.  
  10.     return { 
  11.         postcssPlugin: 'postcss-purge'
  12.         Rule (rule) {} 
  13.     } 
  14.  
  15. module.exports = purgePlugin; 

htmlExtractor 的具體實現就是讀取 html 的內容,對 html 做 parse 生成 AST,遍歷 AST,記錄 id、class、tag:

  1. function htmlExtractor(html, extractInfo) { 
  2.     const content = fs.readFileSync(html, 'utf-8'); 
  3.  
  4.     const extractPlugin = options => tree => {       
  5.         return tree.walk(node => { 
  6.             extractInfo.tag.push(node.tag); 
  7.             if (node.attrs) { 
  8.               extractInfo.id.push(node.attrs.id) 
  9.               extractInfo.class.push(node.attrs.class) 
  10.             } 
  11.             return node 
  12.         }); 
  13.     } 
  14.  
  15.     posthtml([extractPlugin()]).process(content); 
  16.  
  17.     // 過濾掉空值 
  18.     extractInfo.id = extractInfo.id.filter(Boolean); 
  19.     extractInfo.class = extractInfo.class.filter(Boolean); 
  20.     extractInfo.tag = extractInfo.tag.filter(Boolean); 

posthtml 的插件形式和 postcss 類似,我們在 posthtml 插件里遍歷 AST 并記錄了一些信息。

最后,過濾掉 id、class、tag 中的空值,就完成了提取。

我們先不著急做下一步,先來測試下現在的功能。

我們準備這樣一個 html:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <meta http-equiv="X-UA-Compatible" content="IE=edge"
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0"
  7.     <title>Document</title> 
  8. </head> 
  9. <body> 
  10.     <div class="aaa"></div> 
  11.  
  12.     <div id="ccc"></div> 
  13.  
  14.     <span></span> 
  15. </body> 
  16. </html> 

測試下提取的信息:

可以看到,id、class、tag 都正確的從 html 中提取了出來。

接下來,我們繼續(xù)做下一步:從 css 的 AST 中刪掉沒被使用的部分。

我們聲明了 Rule 的 listener,可以拿到 rule 的 AST。要分析的是 selector 部分,需要先根據 “,” 做拆分,然后對每一個選擇器做處理。

  1. Rule (rule) {                         
  2.      const newSelector = rule.selector.split(',').map(item => { 
  3.         // 對每個選擇器做轉換 
  4.     }).filter(Boolean).join(','); 
  5.  
  6.     if(newSelector === '') { 
  7.         rule.remove(); 
  8.     } else { 
  9.         rule.selector = newSelector; 
  10.     } 

選擇器可以用 postcss-selector-parser 來做 parse、分析和轉換。

處理以后的選擇器如果都被刪掉了,就說明這個 rule 的樣式就沒用了,就刪掉這個 rule。否則可能只是刪掉了部分選擇器,該樣式還會被用到。

  1. const newSelector = rule.selector.split(',').map(item => { 
  2.     const transformed = selectorParser(transformSelector).processSync(item); 
  3.     return transformed !== item ? '' : item; 
  4. }).filter(Boolean).join(','); 
  5.  
  6. if(newSelector === '') { 
  7.     rule.remove(); 
  8. else { 
  9.     rule.selector = newSelector; 

接下來實現對選擇器的分析和轉換,也就是 transformSelector 函數。

這部分的邏輯就是對每個選擇器判斷下是否在從 html 提取到的選擇器中,如果不在,就刪掉。

  1. const transformSelector = selectors => { 
  2.     selectors.walk(selector => { 
  3.         selector.nodes && selector.nodes.forEach(selectorNode => { 
  4.             let shouldRemove = false
  5.             switch(selectorNode.type) { 
  6.                 case 'tag'
  7.                     if (extractInfo.tag.indexOf(selectorNode.value) == -1) { 
  8.                         shouldRemove = true
  9.                     } 
  10.                     break; 
  11.                 case 'class'
  12.                     if (extractInfo.class.indexOf(selectorNode.value) == -1) { 
  13.                         shouldRemove = true
  14.                     } 
  15.                     break; 
  16.                 case 'id'
  17.                     if (extractInfo.id.indexOf(selectorNode.value) == -1) { 
  18.                         shouldRemove = true
  19.                     } 
  20.                     break; 
  21.             } 
  22.  
  23.             if(shouldRemove) { 
  24.                 selectorNode.remove(); 
  25.             } 
  26.         }); 
  27.     }); 
  28. }; 

我們完成了 html 中選擇器信息的提取,和 css 根據 html 提取的信息做無用 rule 的刪除,插件的功能就已經完成了。

我們來測試下效果:

css:

  1. .aaa, ee , ff{ 
  2.     color: red; 
  3.     font-size: 12px; 
  4. .bbb { 
  5.     color: red; 
  6.     font-size: 12px; 
  7.  
  8. #ccc { 
  9.     color: red; 
  10.     font-size: 12px; 
  11.  
  12. #ddd { 
  13.     color: red; 
  14.     font-size: 12px; 
  15.  
  16. p { 
  17.     color: red; 
  18.     font-size: 12px; 
  19. span { 
  20.     color: red; 
  21.     font-size: 12px; 

html:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <meta http-equiv="X-UA-Compatible" content="IE=edge"
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0"
  7.     <title>Document</title> 
  8. </head> 
  9. <body> 
  10.     <div class="aaa"></div> 
  11.  
  12.     <div id="ccc"></div> 
  13.  
  14.     <span></span> 
  15. </body> 
  16. </html> 

按理說, p、#ddd、.bbb 的選擇器和樣式,ee、ff 的選擇器都會被刪除。

我們使用下該插件:

  1. const postcss = require('postcss'); 
  2. const purge = require('./src/index'); 
  3. const fs = require('fs'); 
  4. const path = require('path'); 
  5. const css = fs.readFileSync('./example/index.css'); 
  6.  
  7. postcss([purge({ 
  8.     html: path.resolve('./example/index.html'), 
  9. })]).process(css).then(result => { 
  10.     console.log(result.css); 
  11. }); 

經測試,功能是對的:

這就是 PurgeCss 的實現原理。我們完成了 css 的 three shaking!

代碼上傳到了 github:https://github.com/QuarkGluonPlasma/postcss-plugin-exercize

當然,我們只是簡易版實現,有的地方做的不完善:

  • 只實現了 html 提取器,而 PurgeCss 還有 jsx、pug、tsx 等提取器(不過思路都是一樣的)
  • 只處理了單文件,沒有處理多文件(再加個循環(huán)就行)
  • 只處理了 id、class、tag 選擇器,沒處理屬性選擇器(屬性選擇器的處理稍微復雜一些)

雖然沒有做到很完善,但是 PurgeCss 的實現思路已經通了,不是么~

總結

JS 的 TreeShking 使用 Webpack、Terser,而 CSS 的 TreeShking 使用 PurgeCss。

我們實現了一個簡易版的 PurgeCss 來理清了它的實現原理:

通過 html 提取器提取 html 中的選擇器信息,然后對 CSS 的 AST 做過濾,根據 Rule 的 selector 是否被使用到來刪掉沒用到的 rule,達到 TreeShking 的目的。

實現這個工具的過程中,我們學習了 postcss 和 posthtml 插件的寫法,這兩者形式上很類似,只不過一個針對 css 做分析和轉換,一個針對 html。

Postcss 可以分析和轉換 CSS,比如這里的刪除無用 css 就是一個很好的應用。你還見過別的 postcss 的很棒的應用場景么,不妨一起來討論下吧~

 

責任編輯:姜華 來源: 神光的編程秘籍
相關推薦

2022-04-26 08:32:36

CSS前端

2010-08-24 13:34:11

CSSpadding

2020-12-03 08:14:45

Axios核心Promise

2020-11-02 09:35:04

ReactHook

2010-08-25 13:54:29

CSStop

2021-05-13 23:30:17

JavaScript 原理揭秘

2010-09-14 09:24:40

CSS實例

2010-08-26 10:33:27

CSSborder

2019-11-15 15:12:19

Windows激活KMS

2010-08-23 10:43:21

DIVCSS

2010-09-06 09:50:34

id選擇器CSS

2010-09-15 15:03:52

CSS positio

2010-08-16 14:18:49

DIV+CSS

2010-09-17 15:25:03

JAVAJVM

2021-12-01 06:40:32

Bind原理實現

2010-09-03 13:23:07

absoluterelativeCSS

2010-08-31 15:07:45

CSS居中

2010-08-25 08:47:16

CSScellspacingcellpadding

2010-09-02 14:17:56

CSS浮動

2010-09-14 15:32:51

CSSdisplay:inl
點贊
收藏

51CTO技術棧公眾號