webpack默认打包依然存在一些弊端。
就是所有代码最终都会被打包到一起(一个bundle文件中)。
如果项目中代码比较复杂,模块比较多,打包结果就会很大(bundle体积过大),很容易超过2、3MB。
而实际情况是,项目启动时,并不是每个模块都需要加载进来。
但是这些模块又被全部打包到一起,需要任何一个模块都必须整体加载进来后才能使用。
项目运行在浏览器端,这样就会浪费掉很多的流量和带宽。
更为合理的方案,就是「分包」「按需加载」
这样就会大大提高项目的响应速度和运行效率。
前面说webpack就是将代码中散落的模块合并到一起,从而提高运行效率。
现在又要求将它们分离开,这是「物极必反」的结果。资源太大不行,太碎了也不行。
项目中划分模块的颗粒度一般非常的细。
很多时候一个模块只是提供了一个小小的工具函数,并不能形成一个完整的功能单元。
如何不将这些散落的模块合并到一起,在运行一个小小的功能时,就会加载很多的模块。
而当前HTTP1.1版本有很多缺陷,例如:
综上所述,模块打包(合并)是有必要的。
不过在应用越来越大之后,也要慢慢学会变通。
为了解决打包文件过大的问题,webpack支持「代码分包」的功能,也可以称为「代码分割」。
它通过把打包的模块按照我们设计的规则打包到不同的bundle当中,从而提高应用的响应速度。
目前webpack实现 「分包」的方式主要有两种:
import()
,实现模块的按需加载。多入口打包,一般适用于传统的多页应用程序。
最常见的划分规则就是:
webpack配置文件的entry属性配置类型:
每个打包入口会形成一个独立的chunk,入口名称就是这个chunk的name(默认是main)。
一旦配置为多入口,输出的文件名也需要修改:
使用[name]占位符,动态输出文件名,[name]最终替换为入口的名称,即entry的key。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js',
},
output: {
// 使用[name]占位符,动态输出文件名
// [name]最终替换为入口的名称,即entry的key
filename: '[name].bundle.js',
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
}),
new HtmlWebpackPlugin({
filename: 'album.html',
template: './src/album.html',
}),
],
}
此时打包会输出两个html和两个bundle文件,但是发现html文件中将两个bundle文件全部引用了。
这是因为html-webpack-plugin插件会自动生成一个注入所有打包结果的html。
如果需要指定输出的html所使用的bundle,可以使用插件的chunks
属性配置:
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
chunks: ['index'],
}),
new HtmlWebpackPlugin({
filename: 'album.html',
template: './src/album.html',
chunks: ['album'],
}),
每个打包入口,都会形成一个独立的chunk
。
而插件的chunks
属性,通过chunk
的名称,指定需要注入哪些chunk
。
多入口打包缺陷:
不同入口中肯定会有公共的模块,根据现在多入口打包的方式,就会出现在不同的打包结果中,会有相同的模块出现。
例如上面的index.js和album.js都导入了一个公共模块或css模块。
解决方案:
将这些公共的模块单独提取输出到一个公共的bundle中。
webpack可以在optimization优化配置中开启splitChunks功能,实现提取公共模块打包。
optimization: {
splitChunks: {
// all 表示将所有的公共模块都提取到单独的bundle当中
chunks: 'all'
}
}
打包后,就会多生成一个album~index.bundle.js
文件。
它存放的就是index.js和album.js中公共的模块。
「按需加载」是开发浏览器应用中常见的需求。
一般指的按需加载数据。
这里的按需加载指的是:应用运行过程中,需要哪个模块时,才会加载那个模块。
这个方式可以极大的节省带宽和流量。
webpack支持使用动态导入的方式,实现模块的按需加载。
而且所有动态导入的模块,都会被自动分包(形成一个单独的chunk,提取到单独的bundle当中)。
相比多入口方式,动态导入更为灵活。
可以通过代码的逻辑控制是否需要加载某个模块,或何时加载。
使用ESM的import()
方法导入指定的模块,它返回一个promise。
例如:
// 入口文件 /src/index.js
const render = (query) => {
if (query == '#posts') {
import('./posts/posts')
.then(({default: posts}) => {
console.log(posts())
})
} else if (query == 'album') {
import('./album/album')
.then(({default: album}) => {
console.log(album())
})
}
}
// PS:假设posts和album模块中都使用了相同的公共模块
打包会多生成3个文件,文件名为[number].bundle.js
。
分别是posts和album模块,另一个是从二者提取出来的公共模块。
这三个文件就是有动态导入、自动分包生成的。
整个过程不需要配置任何地方,例如不需要配置optimization.splitChunks就可以实现提供公共模块。
只需要按照ESM动态导入成员的方式导入模块即可。
webpack内部会自动处理分包、按需加载以及提取公共模块。
如果使用的是单页应用开发框架(如vue,react),项目中的路由映射组件,就可以通过这种动态导入的方式,实现按需加载。
默认通过动态导入产生的bundle文件,它的名称是一个序号,文件名为[number].bundle.js
。
可以通过webpack特有的魔法注释,给它们定义名称。
具体使用就是,在import()
的参数位置(前后都可以),添加一个特定格式的行内注释:
// 格式:/*webpackChunkName:''*/
import(/* webpackChunkName: 'posts' */'./posts/posts').then(() => {})
import('./posts/posts'/* webpackChunkName: 'album' */).then(() => {})
生成文件:
如果多个模块使用的相同的chunkName,那它们最终会被打包到一起,自然不需要提取公共模块,最终只会生成一个文件。
借助这个特点,就可以根据情况,灵活组织动态导入的模块所输出的文件。