最近在开发一个新的功能,需要在某一条件下引入新包,而之前的场景则完全不需要。这种情况下,动态加载+代码分割正可以派上用场。在动手之前,先看看官网是怎么说的吧。
在entry对象中写入多个入口文件即可。
weboack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
这样做的弊端主要由两个。第一,如果有公共的代码块,那么打包之后的两个Bundle里都会包含重复的模块。第二,不够灵活,不能被webpack通过splitChunks的规则灵活地优化。
因此,在entry里直接写多个入口的方式,适用于本身就是多入口的项目,而非一般的spa。可以通过代码结构,用函数构造出合理的entry和相应的htmlwebpackplugin,来进行打包。
webpack还给出了智能分割代码的功能:splitChunks
,可以用给定的策略去抽取公共代码,避免重复。
webpack.config.js
optimization: {
minimizer: [
new UglifyJsPlugin({
exclude: /\.min\.js$/, // 过滤掉以".min.js"结尾的文件,这个后缀本身就是已经压缩好的代码,没必要进行二次压缩
cache: true,
parallel: true,
sourceMap: true,
extractComments: false,
uglifyOptions: {
output: {
// 移除注释
comments: false,
},
},
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessorOptions: {
parser: safeParser,
autoprefixer: {
disable: true,
},
discardComments: {
removeAll: true, // 移除注释
},
},
canPrint: true,
}),
],
splitChunks: {
minChunks: 3,
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
// 可取'single', 'multiple', default 为false
// 此处等于 runtime的chunkname 即为'runtime'
runtimeChunk: 'single',
}
具体配置写在[optimization.splitChunks ](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks)
中。此外还可以通过mini-css-extract-plugin
,bundle-loader
,promise-loader
等别的社区贡献的工具进行代码分割。
语法分为两种,一种是import()
,另一种是webpack原来的老语法require.ensure
。注意使用import时,要配置对应的Babel插件。
main.js
async function lala() {
const { default: socketcluster } = await import(
/* webpackChunkName: "socket" */
/* webpackMode: "lazy" */
'socketcluster-client')
console.log(typeof socketcluster, socketcluster)
}
lala()
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
],
"env": {
"development": {
"presets": [
"@babel/preset-env"
]
}
}
}
webpack.config.js
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
chunkFilename: '[name].chunk.js',
},
注意main.js
里的注释,这是webpack的魔法注释,可以让chunkFileName
里的name获得注释里写入的值。不过据说webpack5会采用更棒的方式来决定chunkFileName。
魔法注释还包括一些其他的选项,比如:
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackPrefetch: true */
/* webpackPreload: true */
`./locale/${language}`
);
webpackPrefetch
和webpackPreload
: prefetch是未来导航可能需要的资源,preload是当前导航也许需要的资源。当然,这需要浏览器支持,其实就是在浏览器的标签里加入了rel="prefetch"或rel=“preload”。
下载和需求优先级上,preload > prefetch。
检查你的.babelrc
和webpack.config.js
,有没有移除注释的配置。魔法注释,当然注释得存在才能生效。所以.babelrc里不能有comments: false
,webpack的uglifyjs等插件中也不能设置comments: false
和extractComments
。
你可能同时使用了@babel/plugin-syntax-dynamic-import
和 dynamic-import-node
。
@babel/plugin-syntax-dynamic-import
和 dynamic-import-node
在一定程度上是彼此冲突的。后者主要是给Node使用的,把import
语法转译为一个被延迟的require()
。二者的区别已经被讨论过。简单来说,dynamic-import-node
,包括它的babel-7
版本,都是社区贡献的插件。而@babel/plugin-syntax-dynamic-import
是babel官方出品的(看前缀那个@就知道啦),只是为了让babel能够解析动态的import()
,需要配合别的打包工具如webpack,rollup或者原生实现一起使用。
经过打包和压缩(uglify/terser)之后,动态引入部分的代码大小为40.2KB,app主体代码为896 KiB。如果说对于移动端,40KB的代码还值得分离的话,那么再经过gzip压缩后,app主体代码为242KB,分离出的代码大小只有11KB左右,比起多发一个网络请求,倒不如索性打包到一起了。
于是乎,虽然试验了一下code-splitting,但是一番衡量后,最后还是回到了原点。不过这个过程还是蛮有意思的(勉强挽尊吧,哎)。
注意,gzip压缩并不是在webpack打包这一步做的。实际上,webpack
虽然提供了这个选项,比如利用compression-webpack-plugin
,但多数情况下都不会选择使用。这是因为很多流行的静态服务器主机如Surge/Netlify等都已经做了自动gzip静态文件的事情。本项目中,服务端采用了express
,利用了[compress](https://github.com/expressjs/compression)
来进行gzip压缩,减小response body
的大小。所以webpack直接打包出来的文件会比实际上线后,浏览器network
中显示的文件size
要大的多。