CSSTreeShaking原理揭秘:手写一个PurgeCss

CSSTreeShaking原理揭秘:手写一个PurgeCss
最新回答
夏迟归

2024-03-13 07:42:54

TreeShking是通过静态分析的方式找出源码中不会被使用的代码进行删除,达到减小编译打包产物的代码体积的目的。

JS我们会用Webpack、Terser进行TreeShking,而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的threeshaking!

代码上传到了github:https://github.com/QuarkGluonPlasma/postcss-plugin-exercize

当然,我们只是简易版实现,有的地方做的不完善:

只实现了html提取器,而PurgeCss还有jsx、pug、tsx等提取器(不过思路都是一样的)

只处理了单文件,没有处理多文件(再加个循环就行)

只处理了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的很棒的应用场景么,不妨一起来讨论下吧~