CSS TreeShking原理揭秘: 手写 PurgeCss

更新日期: 2021-12-12阅读: 885标签: 原理

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

JS 我们会用 webpack、Terser 进行 Tree Shking,而 css 会用 PurgeCss。

PurgeCss 会分析 html 或其他代码中 css 选择器的使用情况,进而删除没有被使用的 css。

是否对 PurgeCss 怎么找到无用的 css 的原理比较好奇呢?今天我们就来手写个简易版 PurgeCss 来探究下吧。

思路分析

PurgeCss 要指定 css 应用到哪些 html,它会分析 html 中的 css 选择器,根据分析结果来删除没有用到的 css:

const { PurgeCSS } = require('purgecss')
const purgeCSSResult = await new PurgeCSS().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 的分析和转换的。

const purgePlugin = (options) => {

return {
postcssPlugin: 'postcss-purge',
Rule (rule) {}
}
}

module.exports = purgePlugin;

postcss 插件的形式是一个函数,接收插件的配置参数,返回一个对象。对象里声明 Rule、AtRule、Decl 等的 listener,也就是对不同 AST 的处理函数。

这个 postcss 插件的名字叫做 purge,可以被这样调用:

const postcss = require('postcss');
const purge = require('./src/index');
const fs = require('fs');
const path = require('path');
const css = 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 来做提取的事情:

const purgePlugin = (options) => {
const extractInfo = {
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:

function htmlExtractor(html, extractInfo) {
const content = fs.readFileSync(html, 'utf-8');

const extractPlugin = options => tree => {
return tree.walk(node => {
extractInfo.tag.push(node.tag);
if (node.attrs) {
extractInfo.id.push(node.attrs.id)
extractInfo.class.push(node.attrs.class)
}
return node
});
}

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:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="aaa"></div>

<div id="ccc"></div>

<span></span>
</body>
</html>

测试下提取的信息:


可以看到,id、class、tag 都正确的从 html 中提取了出来。

接下来,我们继续做下一步:从 css 的 AST 中删掉没被使用的部分。

我们声明了 Rule 的 listener,可以拿到 rule 的 AST。要分析的是 selector 部分,需要先根据 “,” 做拆分,然后对每一个选择器做处理。

Rule (rule) {                        
const newSelector = rule.selector.split(',').map(item => {
// 对每个选择器做转换
}).filter(Boolean).join(',');

if(newSelector === '') {
rule.remove();
} else {
rule.selector = newSelector;
}
}

选择器可以用 postcss-selector-parser 来做 parse、分析和转换。

处理以后的选择器如果都被删掉了,就说明这个 rule 的样式就没用了,就删掉这个 rule。否则可能只是删掉了部分选择器,该样式还会被用到。

const newSelector = rule.selector.split(',').map(item => {
const transformed = selectorParser(transformSelector).processSync(item);
return transformed !== item ? '' : item;
}).filter(Boolean).join(',');

if(newSelector === '') {
rule.remove();
} else {
rule.selector = newSelector;
}

接下来实现对选择器的分析和转换,也就是 transformSelector 函数。

这部分的逻辑就是对每个选择器判断下是否在从 html 提取到的选择器中,如果不在,就删掉。

const transformSelector = selectors => {
selectors.walk(selector => {
selector.nodes && selector.nodes.forEach(selectorNode => {
let shouldRemove = 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:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="aaa"></div>

<div id="ccc"></div>

<span></span>
</body>
</html>

按理说, p、#ddd、.bbb 的选择器和样式,ee、ff 的选择器都会被删除。

我们使用下该插件:

const postcss = require('postcss');
const purge = require('./src/index');
const fs = require('fs');
const path = require('path');
const css = 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!

代码上传到了 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 就是一个很好的应用。

原文来自:https://mp.weixin.qq.com/s/3-nxykPmocpL57kNy33BHg


链接: https://www.fly63.com/article/detial/10932

CSS定位之BFC背后的神奇原理

BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与

天天都在使用CSS,那么CSS的原理是什么呢?

作为前端,我们每天都在与CSS打交道,那么CSS的原理是什么呢?开篇,我们还是不厌其烦的回顾一下浏览器的渲染过程,学会使用永远都是最基本的标准,但是懂得原理,你才能触类旁通,超越自我。

JavaScript 中的函数式编程原理

做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。

Angular ZoneJS 原理

如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测

Vue.js响应式原理

updateComponent在更新渲染组件时,会访问1或多个数据模版插值,当访问数据时,将通过getter拦截器把componentUpdateWatcher作为订阅者添加到多个依赖中,每当其中一个数据有更新,将执行setter函数

new运算符的原理

一个继承自 Foo.prototype 的新对象被创建;使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数时,Foo 不带任何参数调用的情况

彻底弄懂HTTP缓存机制及原理

Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节,同时对于有志成为前端架构师的同学来说是必备的知识技能。

https的基本原理

HTTPS = HTTP + TLS/SSL,简单理解 HTTPS 其实就是在 HTTP 上面加多了一层安全层。HTTP 可以是 Http2.0 也可以是 Http1.1,不过现在 Http2.0 是强制要求使用 Https 的。使用非对称密钥(即公钥私钥))和对称密钥)(即共享密钥)相结合

Node中的Cookie和Session

HTTP是无状态协议。例:打开一个域名的首页,进而打开该域名的其他页面,服务器无法识别访问者。即同一浏览器访问同一网站,每次访问都没有任何关系。Cookie的原理是

理解Promise原理

Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!