//math.js
export function square(x){
return x*x;
}
export function cube(x){
return x*x*x;
}
//index.js
import {
cube} from './math.js';
const element = document.createElement('pre');
element.innerHTML = '5 cubed is equal to ' + cube(5);
document.body.appendChild(element);
//webpack.config.js
//webpack verson:^5.0.0-beta.17
const path = require('path');
const HtmlWebapckPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin} = require("clean-webpack-plugin");
module.exports = {
mode:'development',
devtool:'cheap-source-map',
// mode:'production',
devServer:{
port:3000,
contentBase:path.join(__dirname,'dist')
},
entry:"./src/index.js",
output:{
filename:"bundle.js",
path:path.join(__dirname,"dist")
},
module:{
rules:[
{
test:/\.js$/,
include:/src/,
exclude:/node_modules/,
use:{
loader:'babel-loader',
options:{
// presets:['@babel/preset-react']
// presets:[
// [
// '@babel/preset-env',{
// modules:false
// }
// ]
// ]
}
}
},
{
test:/\.css$/,
include:/src/,
use:['style-loader','css-loader']
}
]
},
plugins:[
new HtmlWebapckPlugin({
template:'./index.html'
}),
new CleanWebpackPlugin()
]
}
webpack
会对代码作如下标记:
import
标记为 harmony import
export
标记为harmony export
,没引用到的export
标记为unused harmony export
math.js
导出了square
和cube
,但index.js
中只使用了cube
,没有使用square
,所以math.js
中cube
是harmony export
,square
是unused harmony export
。math.js
导出了square
和cube
,而square
并没有被使用。说白了,square
多余了,完全没有必要打包到bundle.js
里,否则徒增代码体积。
将mode
设置为production
,webpack
在打包过程中会将 没有使用的导出内容 排除出最终包。 以上过程就是 tree shaking
。
有两个疑问:
ES Module
(import
/export
)下才可以 tree shaking
?tree shaking
。ES Module
的设计思想就是尽量使代码静态化,使得在编译时就可以确定依赖关系,而不是非得到运行时才确定。mode:'production'
就实现了 tree shaking
?webpack
做了啥?
DefinePlugin
中 process.env.NODE_ENV
的值设置为 production
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
SideEffectsFlagPlugin
ModuleConcatenationPlugin
TerserWebpackPlugin
其中,SideEffectsFlagPlugin
+ModuleConcatenationPlugin
将
没有使用的且没有副作用的模块排除出包(移除未使用的模块)、TerserPlugin
将没有使用的代码语句排除出包(移除未使用的代码)
先举个例子来了解下 SideEffects
,副作用。
新增一个模块文件util.js
。
export function getDate(){
return new Date().toLocaleDateString();
}
在index.js
里导入util.js
的getDate
方法,但不调用。
//index.js
import {
cube} from './math.js';
import {
getDate} from './util.js'
const element = document.createElement('pre');
element.innerHTML = '5 cubed is equal to ' + cube(5);
document.body.appendChild(element);
mode:'production'
,构建的结果里没有util.js
的任何痕迹。也就是说,没有被使用的且没有副作用的模块util.js
被移除了。
现在,在util.js
中添加一句console.log('hello world')
,即
//util.js
export function getDate(){
return new Date().toLocaleDateString();
}
console.log('hello world');
编译后的结果里,从util.js
导入的getDate
方法因为没有被调用,删除肯定不影响功能,所以直接移除就好。但util.js
里的console.log('hello world')
,编译器就无法判断这句到底是干嘛的,保险起见,当然最好不移除。也就是说,console.log('hello world')
被编译器认定为有副作用了,有副作用的东西不能删除。
还有像 修改全局变量如window.name='testing'
等等 都是 副作用。
与 副作用 相反的就是 纯函数。像react
的UI=render(data)
,就是一个纯函数。 输出UI
完全依赖输入data
,只要输入相同,输出就一定相同。
再回到 副作用的 这个例子。
console.log('hello world')
,编译器不知道它是干嘛的,但我们知道啊。我们可以告诉webpack
这句明显是没啥用,完全可以删除它。
怎么告诉?在package.json
里添加"sideEffects":false
。
"sideEffects":false
告诉webpack
,所有的代码都是没有副作用的,没有使用到的你放心删掉就好了。
"sideEffects":false
是不是很妙?不见得。
我们再在index.js
导入index.css
。
//index.css
body{
font-weight:bold;
color:indianred;
}
//index.js
import {
cube} from './math.js';
import {
getDate} from './util.js';
import './index.css';
const element = document.createElement('pre');
element.innerHTML = '5 cubed is equal to ' + cube(5);
document.body.appendChild(element);
情况不妙,样式并没有加上。因为index.css
是没有被使用的代码,且"sideEffects":false
后它已经被webpack
认定是无副作用的,所以index.css
被移除了。
怎么破?有两种方式。
package.json
里的sideEffects
属性"sideEffects":["*.css"]
告诉webpack
,所有的.css
文件都是有副作用的,构建时不要移除//package.json
"sideEffects":["*.css"]
module.rule
里设置sideEffects
,但请注意,是布尔值//webpack.config.js
{
test:/\.css$/,
include:/src/,
use:['style-loader','css-loader'],
sideEffects:true
}
好了,我们已经了解副作用 以及 模块有无副作用该如何标注。
SideEffectsFlagPlugin
则会分析模块之间的依赖关系,没有使用且没有副作用的模块将被打上 无副作用的 标记。
它的作用是作用域提升,更直白一点讲,是将多个模块合并到一个模块里。
开启该功能有两种方式。
//第一种
plugins:[
new webpack.optimize.ModuleConcatenationPlugin()
]
//第二种
optimization:{
concatenateModules :true
}
mode:'development'
和devtool:'cheap-source-map'
下, ModuleConcatenationPlugin
开启前后的对比。
不启用ModuleConcatenationPlugin
:各个模块独占一个闭包;
启用ModuleConcatenationPlugin
:所有模块占一个闭包。
最直观的一个感受是,启用ModuleConcatenationPlugin
后,代码体积变小了些。模块越多,感受越明显。
SideEffectsFlagPlugin
会给 没有被使用且没有副作用的模块打上 无副作用的 标记,而这些被打上标记的模块会被ModuleConcatenationPlugin
跳过,不被合并入最终模块,即被排除出最终包。
TerserWebpackPlugin
会使用terser
压缩js。
以前会用插件UglifyjsWebpackPlugin
,它会使用uglifyjs
压缩js,但因为uglifyjs
仅支持ES5
,而terser
支持ES6+
,所以现在用TerserWebpackPlugin
替代UglifyjsWebpackPlugin
。
前面sideEffects
+SideEffectsFlagPlugin
+ModuleConcatenationPlugin
从模块层面上移除了 没有使用且没有副作用的代码。
TerserPlugin
从代码语句层面上移除了 没有副作用的代码。
/*#__PURE__*/
将函数调用标识为无副作用
还是看例子。
export function getDate(){
return new Date().toLocaleDateString();
}
function sayHi(){
console.log('hello world');
}
sayHi();
mode:'production'
编译后的结果里有console.log('hello world')
。
现在,我们在sayHi
函数调用前添加这么一行注释/*#__PURE__*/
,`即
export function getDate(){
return new Date().toLocaleDateString();
}
function sayHi(){
console.log('hello world');
}
/*#__PURE__*/sayHi();
/*#__PURE__*/
将函数调用标识为无副作用。
瞧,无副作用的sayHi()
被移除了。
pure_funcs将函数标记为无副作用
除了/*#__PURE__*/
这个方法外,还可以用pure_funcs
将sayHi
标记函数为无副作用。
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
devtool:'cheap-source-map',
mode:'production',
optimization: {
concatenateModules:true,
minimize: true,
minimizer:[
new TerserPlugin({
terserOptions:{
compress:{
pure_funcs:['sayHi'],
drop_console:true
}
}
})
]
}
}
terserOptions.compress
对象里可以设置很多属性,可以到这里看看。
在开发阶段我们可能会使用很多console.log
打印日志,但发布的时候肯定要删除这些,将terserOptions.compress.drop_console
设置为true
就好。
Tree Shaking in Webpack
代码体积减少80%!Taro H5转换与优化升级
UglifyjsWebpackPlugin
TerserWebpackPlugin
UglifyJS
terser