摇树(tree shaking)
我主要是想说摇树失败的原因(tree shaking 失败的原因),先讲下摇树本身效果
什么是摇树?
举个例子
首先 webpack.config.js配置
const webpack = require("webpack");/*** @type {webpack.Configuration}*/
module.exports = {mode: "production"
};
在固定 a.js 用esm导出,b.js用commonjs导出不变动
// a.js
export function f1() {console.log("11111");
}
export function f2() {console.log("22222");
}// b.js
exports.f3 = function () {console.log("33333");
};
exports.f4 = function () {console.log("44444");
};
例子1:import a.js 和 require b.js
// index.js
import { f1 } from "./a";
import { f3 } from "./b";
console.log(f1);
console.log(f3);
打包结果:a.j 和 b.js 都摇树了,只输出了 f1 和 f3。所以导入用import,导出esm和commonjs都可以
例子2:import a.js 和 import b.js
// index.js
import { f1 } from "./a";
const { f3 } = require("./b");
console.log(f1);
console.log(f3);
打包结果:a.js 摇,b.js 没摇,输出了 f1 、f3、f4。所以导入用require不成功
结论:
摇树只能import,导出用esm和commonjs都可以
因为摇树发生在编译阶段
,只支持esm的import,不支持commonjs的require,因为esm是编译时,commonjs是运行时
摇树失败的原因
三方面可能导致失败:
1、代码没用import引入
2、webpack配置没开启摇树
3、副作用(sideEffects)
4、babel配置preset-env没写 module:false 参数
代码没用import引入
这一点上面已经说明,必须用 import 导入,导出用 esm 或者 commonjs 都行
webpack配置没开启摇树
开启摇树两步:
1、usedExports设置true,标记无用代码,esm导出的没使用到的导出函数标记为unused harmony export f2
,commonjs导出的没使用的导出函数赋值为__webpack_unused_export__
2、terser-webpack-plugin插件做代码压缩去除无用代码,根据一步两种标记,压缩代码会去除
const webpack = require("webpack");/*** @type {webpack.Configuration}*/
module.exports = {mode: "none",optimization:{usedExports:true}
};
mode: production
模式下,默认开启摇树,不用做任何配置,由源码看出none
和development
不会开启摇树,需要手动加这两步,注意要设置minimize:true
,或者放到plugins中
看webpack源码默认配置,参考 前端进阶面试题详细解答
副作用(sideEffects)
先来解释下什么是副作用:修改当前作用域之外的行为都叫副作用
,比如在函数内部,修改dom,修改全局对象等等
这条主要是针对引入三方包,三方包package.json的sideEffects字段默认true表示有副作用
,可以设置为false表示没有副作用,设置为数组列出有副作用的文件
在webpack.config.js设置sideEffects:true表示检查三方包的sideEffects字段
,webpack在用userExports标记无用代码时,如果判断不出库中代码是否有副作用,就不会标记,则压缩的时候也没法清除,如果判断有副作用,则更不会标记清除
mode: production
模式下,默认开启摇树,不用做任何配置,usedExports: true
const webpack = require("webpack");
const TerserPlugin = require("terser-webpack-plugin");/*** @type {webpack.Configuration}*/
module.exports = {mode: "none",optimization:{sideEffects:true,usedExports:true},plugins:[ new TerserPlugin() ]
};
babel配置preset-env没写 module:false 参数
在文章 我掌握的Babel配置 中详细讲解了 module: false 参数,简单说不设置false时,只针对babel相关的runtime包
的引入会使用require,设置了false引入会使用import,就能让webpack去摇树,回到第一点上
module.exports = {presets: [ [ "@babel/preset-env", { modules: false }, ],]
};
拆包(splitChunks)
splitChunks是webpack配置下optimization下的配置,即优化。看单词理解意思就是拆分多个chunk。
什么是chunk
webpack的本质是把多个js模块合并到一个js中,即一个入口得到一个输出js文件(bundle.js)。
但是导致的问题是,如果这个bundle.js文件很大,那么浏览器请求的时候,导致请求时间很长,首屏长时间白屏。
所以优化手段就是把bundle.js文件拆分成多个小的js文件,同时请求,首屏当然就更快渲染显示。
所以入口文件,chunk文件,输出文件三者的关系从原来的一个入口文件对应一个chunk最后输出一个bundle文件
改变为一个入口文件对应多个chunk最后输出多个bundle文件
三种方式获得chunk
-
1、入口文件可以生成chunk,入口文件即webpack配置的
entry
选项; -
2、异步请求 import函数调用 或者 require.ensure 可以生成chunk;
如:import函数即我们在写vue-router时写的异步请求路由方式,这里webpackChunkName
可以魔法定义chunk名,也可不写
import(/* webpackChunkName: "AboutPage" */'./view/about.vue')
- 3、webpack配置splitChunks手动拆分生成chunk,最后独立输出到js文件
splitChunks 配置
简单配置,把react相关包都单独提到一个文件
{optimization: {splitChunks: {chunks: "all", // initial、async和allcacheGroups: {react: {name: "react",test: /[\\/]react(\w)*[\\/]/i,priority: 10},lodash: {name: "lodash",test: /[\\/]lodash(\w)*[\\/]/i,priority: 20,minChunks:3},},},},
}
先来看下webpack默认的splitChunks参数
看图production
和非production
模式下有参数不一样,下面这些参数表示自动拆包的条件:
- chunks
重要:
拆包的范围,默认async,只针对异步请求的,即上面第二条的import函数调用的chunk里面;initial表示只针对初始化入口entry的;all表示最大包含async + entry
- cacheGroups
重要:
自定义拆包规则,name是chunk名,test正则包名,priority优先级(因为同一个包可能符合多个拆包规则,会处理给优先级高的);看图可知,默认会有两个包规则,defaultVendors
规则表示node_modules
会拆到一个chunk包,default
规则表示只有被两个即以上chunk引用就要拆到一个chunk包
- minChunks
拆分前必须共享模块的最小 chunks 数,可以不用修改
- maxAsyncRequests
浏览器发送异步请求时,最大不超过30个请求,即上面第二条的import函数调用,可以不用修改
- maxInitialRequests
浏览器请求入口entry时,最大不超过30个,可以不用修改
热更新
我们主要是说明热更新的 module.hot.accept()
先来了解一下热更新怎么配置的?
热更新配置
装包
npm i -D webpack-dev-server html-webpack-plugin
webpack.config.js
const webpack = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin');/*** @type {webpack.Configuration}*/
module.exports = {mode: "development",devServer: {port: 3000,open: true,hot: true,},plugins: [ new HtmlWebpackPlugin(), ]
};
package.json
"scripts": {"serve": "webpack serve",
},
结论
到此热更新配置完成,正常写代码,但是发现问题了,此时更新页面是整个刷新页面的,并不是局部刷新
,怎么回事呢,原来需要在每个文件中最后加上module.hot.accept()
才会触发局部更新,accept可以接受两个参数,依赖和回调
exports.f3 = function () {console.log("33333");
};
exports.f4 = function () {console.log("44444");
};
if (module.hot) {module.hot.accept();
}
随即产生了另一个疑问,这太麻烦了吧,每个文件文件都需要去加module.hot.accept()
,但是我们在实际写下项目的时候怎么没有写这句呢?
原因是不论css、vue、react的loader都帮我们自动加了这句。
css有style-loader,react有react-hot-loader,vue有vue-loader。
对于jsx文件,有vue-jsx-hot-loader
{test:/\.jsx?$/,use:['babel-loader','vue-jsx-hot-loader']
}
按需加载
一段时间以来,我一直把tree shaking和按需加载混为一谈,其实应该分开理解,这里我主要是想说第三方包的按需加载,比如使用element-ui、lodash、vant
tree shaking的前提是使用import导入,但是按需加载并不需要
还有一个点需要注意:如果是我们封装的库,如组件库,导出格式根据文件类型不同,如是js文件可以为 commonjs + es5、esm + es5;如是vue或react文件,esm/commonjs + es6/es5 任意都行,因为我们用babel-loader时会排除node_modules目录不编译,vue-loader等会去编译vue文件
使用babel插件
npm install babel-plugin-component -D
babel.config.js
module.exports = {presets: [["@babel/preset-env",{useBuiltIns: "usage",corejs: 2,modules: false,},],],plugins: [["@babel/plugin-transform-runtime"],["babel-plugin-import",{libraryName: "vant",libraryDirectory: "es",style: true,},"vant",],["babel-plugin-import",{libraryName: "antd",style: true, // or 'css'},],["babel-plugin-import",{"libraryName": "lodash","libraryDirectory": "","camel2DashComponentName": false, // default: true},"lodash",],["babel-plugin-component",{libraryName: "element-ui",styleLibraryName: "theme-chalk",},"element-ui",],],
};
完毕!