自从webpack4
以后,官方帮我们集成了很多特性,比如在生产模式下代码压缩
自动开启等,这篇文章我们一起来探讨一下webpack
给我们提供的高级特性助力开发。
探索webpack的高级特性
特性:treeShaking
顾名思义treeShaking
,就是摇树,那么体现在代码模块里面就是摇掉那些没有被外部成员引用的代码,指的注意的是在生产环境下treeShaking
会自动开启。
treeShaking初体验
比如我们在代码中引入lodash
库,我们只用到了once
方法,那关于lodash
其他的功能模块,在生产环境下打包,并不会输出到bundle.js
文件里面,比如我们在bundle.js
里面去找lodash
的一个方法debounce
,他是完全可以找得到的。
delelopment模式下打包的bundle.js
production模式下打包的bundle.js
在这里你可能会说了production
模式下会开启n
多插件,处理打包结果,怎么就能说明是treeShaking
做的呢,确实这种做法不能说明是treeShaking
做的,我们可以把mode
设置为none
再试一下,不过这里需要我们手动去开启treeShaking
,开启的方式如下。
// webpack.config.js
module.exports = {
...
optimization: {
usedExports: true, // 只导出外部成员引用模块
// 此属性用于模块导入合并,因为单独的模块导入要使用_webpack_require_函数。
// 此属性就是可以利用_webpack_require_一次性导入所有模块,也叫作用域提升。
concatenateModules: true,
minimize: true, // 开启代码压缩
}
...
}
none模式下打包的bundle.js
所以none
模式下,打包的结果依然如此。
扩展
因为treeShaking
是依赖于ESM
的,如果项目中有配合使用babel-loader
那么treeShaking
是不是会失效呢?我们可以在配置文件里面添加babel-loader
来辅以测试。
// 安装
npm i babel-loader @babel/core @babel/preset-env -D
// webpack.config.js
module.exports = {
...
module:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env']
]
}
}
}
]
}
文件效果
我们可以看到没有使用的代码,依然是被移除掉了。
原因分析
因为babel-loader
禁用了对ESM
转化插件,所以经过babel-loader
处理生成的依旧是ESM
代码,如果你想使用代码转换功能,那你就需要像下面这样配置,只不过这样treeShaking
就会失效了。
// 安装
npm i babel-loader @babel/core @babel/preset-env -D
// webpack.config.js
module.exports = {
...
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[
// 强制使用commonjs转换
['@babel/preset-env', {modules: 'commonjs'}]
]
}
}
}
]
}
}
那么treeShaking
失效了,应该怎么办?不要怕,即使失效了还会有其他插件提供了类似treeShaking
功能,比如代码压缩。
特性: sideEffect
sideEffect
表示的意思就是副作用,理解起来并不难,比如外部成员引用了当前模块,那么当前模块肯定是不会被treeShaking
的,如果在当前模块里面写了冗余的代码,那么sideEffect
就是去除这些冗余代码的,以达到更高的提效能力。
sideEffect的基础实践
这里我们应该在webpack.config.js
里面开启sideEffect
,在package.json
里面指定具有副作用的模块。
// webpack.config.js
module.exports = {
...
optimization: {
sideEffect: true
}
...
}
// package.json
{
"scripts": {},
"sideEffect": [
// 告知webpack此文件具有副作用
"./src/app.js",
// *通配符css文件
"*.css"
]
}
特性: CodeSplitting分包策略
CodeSplitting
分包策略旨在解决单入口打包导致bundle.js
文件过大,从而导致浏览器http
加载速度过慢造成页面短暂白屏
情况,分包策略
具有三种常见实施方式。
- 根据项目背景,多入口打包。
- 结合
ESM
的Dynamic import
特性,按需加载模块。 - 对第三方包使用拆包策略。
多入口打包的具体实践
多入口打包体现在多页应用,每一个页面依赖于一个打包文件,对于模块中的公共代码进行提取到公共结果中。
module.exports = {
entry: {
index: "./src/index.js",
add: "./src/add.js",
},
optimization: {
splitChunks: {
// 自动提取到一个公共的bundle.js中
chunks: "all"
}
}
plugins:[
...
new HtmlWebpackPlugin({
filename: 'anout.html',
template: './aout.html',
chunks:['add']
}),
new HtmlWebpackPlugin({
titie: 'title',
template: './index.html',
meta:{
viewport: 'width=device-widt, initial-scale=2.0'
},
filename: 'index.html',
publicPath: './',
scriptLoading: 'module',
chunks:['index']
})
...
],
...
}
比如在index.js、add.js
里面抖音用到了once
方法,webpack
就会提取公共的lodash
到单的文件里面,在两个页面里面会通过script
引入。
Dynamic import的按需加载实践
在选项卡切换场景下,在应用程序运行的过程中,只有当用户点击某个模块,才会对应去加载某个模块,大大的减少了启动时需要加载模块的体积,降低了浏览器网路的带宽的占用,提高了应用的响应率。
const hash = window.location.hash;
const container = document.getElementById('app');
switch(hash){
case 'title_1':
import('./title_1.js').then({default:title_1}=>{
container.appednChild(title_1())
});
break;
case 'title_2':
import('./title_2.js').then({default:title_1}=>{
container.appednChild(title_2())
});
break;
case 'title_3':
import('./title_3.js').then({default:title_1}=>{
container.appednChild(title_3())
});
break;
default:
}
按需加载
确实不需要在首屏的时候一次性把文件全部
加载完毕,因为首屏并不需要
所有模块,加载了也是浪费
。
第三方包拆包策略
所谓三方包,在在多入口里面也提到过optimization.splitChunks
只是一种提取三方包的方式,我们现在要讲的是插件层面的DllPlugin
和DllReferencePlugin
,这个插件的意义更为广阔一点,比如类似vue
,react
等三方包,配合着我们的项目代码,只需要初次构建一次,再次构建webpack
就会跳过这些依赖包,只要我们不手动升级依赖包,那将会是永久性的缓存。
使用步骤
- 新建
webpck.dll.config.js
文件,写上如下内容。
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
// 入口文件
mode: "development",
entry: {
// 项目中用到该两个依赖库文件
lodash: ['lodash'],
},
// 输出文件
output: {
// 文件名称
filename: '[name].dll.js',
// 将输出的文件放到dist目录下
path: path.resolve(__dirname, 'vandor'),
/* 存放相关的dll文件的全局变量名称,比如对于lodash来说的话就是 _dll_lodash, 在前面加 _dll 是为了防止全局变量冲突。 */
library: '_dll_[name]'
},
plugins: [
// 使用插件 DllPlugin
new DllPlugin({
/* 该插件的name属性值需要和 output.library保存一致,该字段值,也就是输出的 manifest.json文件中name字段的值。 比如在jquery.manifest文件中有 name: '_dll_jquery' */
name: '_dll_[name]',
/* 生成manifest文件输出的位置和文件名称 */
path: path.join(__dirname, 'vandor', '[name].manifest.json')
})
]
};
- 在package.json里面新增命令 =>
"dll": "webpack --config webpack.dll.config.js"
并执行,生成文件。
- 引入文件的依赖关系
const webpack = require('webpack');
module.exports = {
plugins:[
...
new webpack.DllReferencePlugin({
manifest: require('./vandor/lodash.manifest.json')
}),
...
]
}
特性: 魔法注释
在分包或者定义其他模块的时候,我们想给模块定义一个名称,那就可以使用如下方式。
/* webpackChunkName:'<chunkName>' */
探索webpack带来的前端性能优化
在前几篇文章里面我们就知道了webpack
通过mode
来提供了none
、development
、production
三种预设配置。每一种配置都会选择性的加载某些插件来优化项目的构建,但是作为一个开发者我们应当去关注非自动的功能配置,下面我们来一起探索一下在开发中使用到的配置能带来一定的性能优化
。
为什么要进行性能优化
性能优化是前端开发的永久性话题,高性能应用的开发这是我们的目标,但是目标总就是目标,具体实施还是要一步一块板砖,webpack
在实践如此多的新特性的同时,会给我们的打包结果带来具有影响的内容,比如sourceMap
,上有政策下有对策,那么我们的种种可优化的点就是解决问题的对策。
具体对策
那么我们应该怎么样来提高构建速度与打包结果呢?
实际的开发中你总会见到我们会对不同的环境配置不同的文件,根据env
的不同来启用不同的配置。
// webpack.development.config.js
module.exports = {
mode:"development",
detool:"source-map"
...
}
// webpack.production.config.js
module.exports = {
mode:"production",
detool:"nosources-source-map"
...
}
// webpack.config.js
module.exports = (env, args) => {
// 公共配置
const config = {
module:{},
plugins:[],
...
}
env === "development" ? require('./webpack.development.config.js'):require('./webpack.production.config.js')
return config;
}
-
DefinePlugin
定义全局变量,可用作baseUrl
。... plugin:[ new webpack.DefinePlugin({ API_BASEURL:'https://www.yixizhishi.com' }) ] ...
-
MiniCssExtractplugin
用来从js代码中提取css代码。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
...
plugin:[
new MiniCssExtractPlugin()
],
module:{
rules:[
{
test:/\.css/,
use:[
// 通过link标签引入到页面中
MiniCssExtractPlugin.loader,
cssloader
]
}
]
}
-
optimizeCssAssetsWebpackPlugin
,用来压缩css
代码。webpack
中所谓压缩就是压缩js
文件的,而css
文件,需要我们单独处理。
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
...
// 非plugin中使用
optimization:[
minimizer:[
new OptimizeCssAssetsWebpackPlugin()
new terser-webpack-plugin()
]
]
terser-webpack-plugin
用于压缩js
代码。- 如果在
optimization
选项中开启了minimizer
属性,则会覆盖掉webpack
本身的压缩功能,所以我们需要手动添加压缩插件。
- 如果在
const terserWebpackPlugin = require('terser-webpack-plugin');
...
optimization:[
minimizer:[
// 压缩css
new OptimizeCssAssetsWebpackPlugin()
// 压缩js
new terserWebpackPlugin()
]
]
当然还有一些其他的配置呀,比如。
splitChunks
的一些配置呀,也就是按你的需求拆包呀。
splitChunks: {
cacheGroups: {
commons: {
chunks: "initial",//相同的chunks提出来
minChunks: 2,//依赖了两个以上的关系
minSize: 0 //这个依赖最小体积为0
},
vendor: {
test: /node_modules/,
// 默认选项,表示只要有依赖的第三方包就要拆出去,跟all差不多
chunks: "initial",
name: "vendor",
enforce: true
}
},
}
cdn
的引入三方包呀。
module.exports = {
...
// 通过外部引入第三方包
externals:['jQuery','lodash']
...
}
多线程
打包的开启呀,比如happyPack
。happyPack
的工作原理就是把loader
加载分配多个线程去处理,最后在统一调度起来,处理完成之后通知webpack
进行chunks
的组合,输出bundle.js
。 注意:并不是说多进程打包就一定好,因为创建多线程的时候也会有性能开销,所以还是斟酌而行。
- 使用
include
避免webpack
处理不需要处理的模块文件,提高编译效率。 - webpack5提供了webpack资源模块,来代替一般的
loader
处理文件,好处是能够处理不同类型的文件并且不再需要针对性的配置loader
。
- resolve模块一般被人们忘掉了,不过在vue/react的脚手架中还是看见过它的身影,一般用于告诉webpack以什么样的形式去处理文件,比如。
- 别名:
alias
- 文件类型:
extensions
- 解析的模块范围:
modules
- 别名:
module.exports = {
resolve: {
alias:{
'@':'root/src' // 指定别名@,通过@可以找到文件目录
},
extensions:{
['.jsx', '.tsx', '.vue'] // 指定webpack需要解析哪些类型的文件
},
modules:{
['node_modules', 'root/src'] // 指定webpack需要解析那些范围的文件
}
}
}
写在最后
因为上面的一些优化手段涵盖了webpack5
以及webpack5
以前的特性,那么在这里提及一下webapck5
中开箱即用的特性以及不再维护
的老版本的特性吧。
- 持久化缓存,使用
cache
之后我们便不需要使用dll
拆包、cache-loader
了,而且是webpack5
中提供的功能。
module.exports = {
cache: {
type: 'filesystem', // 文件系统
},
}
thread-loader
开启多线程打包,上述代码中提到了happypack
,不过在webpack5
当中,已经不再去维护happypack
了,我们就应该使用thread-loader
来加快构建进程。
总结
上述讲解的内容均是在开发环境下的的配置的一步步实现,当然在mode:"production"
下webpack
会自动帮我们做,所以在不依赖别人的情况下,还是自己配比较好玩。下一章我们就一起来探索一下各大成熟框架是怎么配置webpack
的