下面介绍一下原理
这是一个 babel 的插件,babel 通过核心 babylon 将 ES6 代码转换成 AST 抽象语法树,然后插件遍历语法树找出类似 import {Button} from 'element-ui'这样的语句,进行转换,最后重新生成代码。
babel-plugin-import-fix 默认支持 antd,element,meterial-UI,wui,xcui 和 d3,只需要再. babelrc 中配置插件本身就可以。
.babelrc
- {
- "presets": [["es2015", {
- "modules": false
- }], "react"],
- "plugins": ["import-fix"]
- }
其实是想把所有常用的库都默认支持,但很多常用的库却不支持缩小引用范围。因为没有独立输出各个子模块,不能把引用修改为对单个子模块的引用。
我们前面所说的 tree-shaking 都是针对 js 文件,通过静态分析,尽可能消除无用的代码,那对于 css 我们能做 tree-shaking 吗?
随着 CSS3,LESS,SASS 等各种 css 预处理语言的普及,css 文件在整个工程中占比是不可忽视的。随着大项目功能的不停迭代,导致 css 中可能就存在着无用的代码。我实现了一个 webpack 插件来解决这个问题,找出 css 代码无用的代码。
webpack-css-treeshaking-plugin ,对 css 进行 tree-shaking webpack-css-treeshaking-plugin下面介绍一下原理
整体思路是这样的,遍历所有的 css 文件中的 selector 选择器,然后去所有 js 代码中匹配,如果选择器没有在代码出现过,则认为该选择器是无用代码。
首先面临的问题是,如何优雅的遍历所有的选择器呢?难道要用正则表达式很苦逼的去匹配分割吗?babel 是 js 世界的福星,其实 css 世界也有利器,那就是 postCss。
PostCSS 提供了一个解析器,它能够将 CSS 解析成 AST 抽象语法树。然后我们能写各种插件,对抽象语法树做处理,最终生成新的 css 文件,以达到对 css 进行精确修改的目的。
整体又是一个 webpack 的插件,架构图如下:
主要流程:
- apply (compiler) {
- compiler.plugin('after-emit', (compilation, callback) => {
- let styleFiles = Object.keys(compilation.assets).filter(asset => {
- return /\.css$/.test(asset)
- })
- let jsFiles = Object.keys(compilation.assets).filter(asset => {
- return /\.(js|jsx)$/.test(asset)
- })
- ....
- }
- let tasks = [] styleFiles.forEach((filename) = >{
- const source = compilation.assets[filename].source() let listOpts = {
- include: '',
- source: jsContents,
- //传入全部js文件
- opts: this.options //插件配置选项
- }
- tasks.push(postcss(treeShakingPlugin(listOpts)).process(source).then(result = >{
- let css = result.toString() // postCss处理后的css AST
- //替换webpack的编译产物compilation
- compilation.assets[filename] = {
- source: () = >css,
- size: () = >css.length
- }
- return result
- }))
- })
- module.exports = postcss.plugin('list-selectors', function (options) {
- // 从根节点开始遍历
- cssRoot.walkRules(function (rule) {
- // Ignore keyframes, which can log e.g. 10%, 20% as selectors
- if (rule.parent.type === 'atrule' && /keyframes/.test(rule.parent.name)) return
- // 对每一个规则进行处理
- checkRule(rule).then(result => {
- if (result.selectors.length === 0) {
- // 选择器全部被删除
- let log = ' ✂️ [' + rule.selector + '] shaked, [1]'
- console.log(log)
- if (config.remove) {
- rule.remove()
- }
- } else {
- // 选择器被部分删除
- let shaked = rule.selectors.filter(item => {
- return result.selectors.indexOf(item) === -1
- })
- if (shaked && shaked.length > 0) {
- let log = ' ✂️ [' + shaked.join(' ') + '] shaked, [2]'
- console.log(log)
- }
- if (config.remove) {
- // 修改AST抽象语法树
- rule.selectors = result.selectors
- }
- }
- })
- })
checkRule 处理每一个规则核心代码
- let checkRule = (rule) => {
- return new Promise(resolve => {
- ...
- let secs = rule.selectors.filter(function (selector) {
- let result = true
- let processor = parser(function (selectors) {
- for (let i = 0, len = selectors.nodes.length; i < len; i++) {
- let node = selectors.nodes[i]
- if (_.includes(['comment', 'combinator', 'pseudo'], node.type)) continue
- for (let j = 0, len2 = node.nodes.length; j < len2; j++) {
- let n = node.nodes[j]
- if (!notCache[n.value]) {
- switch (n.type) {
- case 'tag':
- // nothing
- break
- case 'id':
- case 'class':
- if (!classInJs(n.value)) {
- // 调用classInJs判断是否在JS中出现过
- notCache[n.value] = true
- result = false
- break
- }
- break
- default:
- // nothing
- break
- }
- } else {
- result = false
- break
- }
- }
- }
- })
- ...
- })
- ...
- })
- }
可以看到其实我只处理里 id 选择器和 class 选择器,id 和 class 相对来说副作用小,引起样式异常的可能性相对较小。
判断 css 是否再 js 中出现过,是使用正则匹配。
其实,后续还可以继续优化,比如对 tag 类的选择器,可以配置是否再 html,jsx,template 中出现过,如果出现过,没有出现过也可以认为是无用代码。
当然,插件能正常工作还是的有一些前提和约束。我们可以在代码中动态改变 css,比如再 react 和 vue 中,可以这么写
这样是比较推荐的方式,选择器作为字符或变量名出现在代码中,下面这样动态生成选择器的情况就会导致匹配失败
- render(){
- this.stateClass = 'state-' + this.state == 2 ? 'open' : 'close'
- return <div class={this.stateClass}></div>
- }
其中这样情况很容易避免
- render(){
- this.stateClass = this.state == 2 ? 'state-open' : 'state-close'
- return <div class={this.stateClass}></div>
- }
所以有一个好的编码规范的约束,插件能更好的工作。
如果 webpack 打包后的 bundle 文件中存在着相同的模块,也属于无用代码的一种。也应该被去除掉
首先我们需要一个能对 bundle 文件定性分析的工具,能发现问题,能看出优化效果。
webpack-bundle-analyzer 这个插件完全能满足我们的需求,他能以图形化的方式展示 bundle 中所有的模块的构成的各构成的大小。
其次,需求对通用模块进行提取,CommonsChunkPlugin 是最被人熟知的用于提供通用模块的插件。早期的时候,我并不完全了解他的功能,并没有发挥最大的功效。
下面介绍 CommonsChunkPlugin 的正确用法
自动提取所有的 node_moudles 或者引用次数两次以上的模块minChunks 可以接受一个数值或者函数,如果是函数,可自定义打包规则
但使用上面记载的配置之后,并不能高枕无忧。因为这个配置只能提取所有 entry 打包后的文件中的通用模块。而现实是,有了提高性能,我们会按需加载,通过 webpack 提供的 import(...)方法,这种按需加载的文件并不会存在于 entry 之中,所以按需加载的异步模块中的通用模块并没有提取。
如何提取按需加载的异步模块里的通用模块呢?配置另一个 CommonsChunkPlugin,添加 async 属性,async 可以接受布尔值或字符串。当时字符串时,默认是输出文件的名称。
names 是所有异步模块的名称
这里还涉及一个给异步模块命名的知识点。我是这样做的:
- const Edit = resolve => { import( /* webpackChunkName: "EditPage" */ './pages/Edit/Edit').then((mod) => { resolve(mod.default); }) };
- const PublishPage = resolve => { import( /* webpackChunkName: "Publish" */ './pages/Publish/Publish').then((mod) => { resolve(mod); }) };
- const Models = resolve => { import( /* webpackChunkName: "Models" */ './pages/Models/Models').then((mod) => { resolve(mod.default); }) };
- const MediaUpload = resolve => { import( /* webpackChunkName: "MediaUpload" */ './pages/Media/MediaUpload').then((mod) => { resolve(mod); }) };
- const RealTime = resolve => { import( /* webpackChunkName: "RealTime" */ './pages/RealTime/RealTime').then((mod) => { resolve(mod.default); }) };
没错,在 import 里添加注释。/* webpackChunkName: "EditPage" */ ,虽然看着不舒服,但是管用。
贴一个项目的优化效果对比图
优化效果还是比较明显。
优化前 bundle
来源: https://juejin.im/post/5a4dca1d518825128654fa78