CSS TreeShking原理揭秘: 手写 PurgeCss

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

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://fly63.com/article/detial/10932

相关推荐

React Native之原理浅析

讲React Native之前,了解JavaScriptCore会有帮助,也是必要的。React Native的核心驱动力就来自于JS Engine. 你写的所有JS和JSX代码都会被JS Engine来执行, 没有JS Engine的参与,你是无法享受ReactJS给原生应用开发带来的便利的

连v-show都不会你还敢说熟悉 Vue 原理?

Vue 作为最主流的前端框架,中文资料齐全、入门简单、生态活跃,可以说是工作中最常用的,如今对 Vue 原理的熟悉基本上是简历的标配了。之前参与了部分 2019 校园招聘的面试工作,发现很多简历上都写了:

Angular ZoneJS 原理

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

js中flat方法的实现原理

Array.prototype.flat()在Array的显示原型下有一个flat方法,可以将多维数组,降维,传的参数是多少就降多少维,自定义flat的步骤1、第一步是类型判断,需要判断当前调用方法的this是否为一个数组,若不是数组则返回undefined

JavaScript 中的函数式编程原理

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

理解Vue的Watch原理

watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。

写一个简单的vue-router来剖析原理

随着前端业务的发展, 我们一般在写一个较为大型的vue项目时候,会使用到vue-router,来根据指定的url或者hash来进行内容的分发,可以达到不像服务端发送请求,就完成页面内容的切换,能够减少像服务器发送的请求

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

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

前端手写代码原理实现

现在的前端门槛越来越高,不再是只会写写页面那么简单。模块化、自动化、跨端开发等逐渐成为要求,但是这些都需要建立在我们牢固的基础之上。不管框架和模式怎么变,把基础原理打牢才能快速适应市场的变化。下面介绍一些常用的源码实现

关于vue过滤器的原理解析

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部

点击更多...

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