webpack非常受欢迎的一个特性之一就是代码分离,能把一个js按照需求拆分到不通的js中,按需加载或者并行加载这些文件,可以用来控制资源加载优先级,降低首屏加载时间等等,今天就来总结下,webpack中拆分js的几种方式,以及他们的特点和使用场景。
一、Entry point
webpack配置对象中的entry是最简单的、最直观的代码拆分方式,但是这种适用场景也比较有限,主要是用来配置多入口页面。
a.js
import com from './common'
console.log(`a中引入的${com}`)
console.log('a.js')
b.js
import com from './common'
console.log(`b中引入的${com}`)
console.log('b.js');
common.js
import _ from 'lodash';
console.log(
_.join(['hello', 'common', 'loaded!'], ' ')
);
export default 1
webpack.config.js
const path = require('path')
module.exports = {
devtool: 'none',
mode: 'development',
entry: {
index: './a.js',
another: './b.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve('dist')
}
}
编译产出
可以看到共生成了2个js,能够实现入口的js拆分,这里有个缺陷就是,他们共同依赖的common.js 在2个入口文件中都被打入进去,编译了2次,这个就是入口拆分的局限,再webpack4以前,我们可以通过如下配置解决重复依赖
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common' // 指定公共 bundle 的名称。
})
],
但是在webpack4以后,CommonsChunkPlugin被移除,取而代之的是 optimization.splitChunks(后面会做详细介绍)
const path = require('path')
module.exports = {
devtool: 'none',
mode: 'development',
entry: {
a: './a.js',
b: './b.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve('dist')
},
optimization: {
splitChunks: {
chunks: 'initial', // 同步的js切割,默认是异步
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 正则匹配文件,这里匹配的是node_modules
},
},
},
},
}
这样将入口文件的重复js打包成一个,我们看下编译结果
共产出了3个js,除了2个入口文件,针对共同依赖的common.js中的lodash,做了单独的打包。
总结
- entry可以实现代码分隔
- entry 更适合多入口应用的代码分隔,默认情况下,多入口的共同依赖会分别打包到各入口文件中,造成重复引用
- 多入口分隔的重复引用模块,可以用optimization.splitChunks作拆分
二、optimization.runtimeChunk
将 optimization.runtimeChunk 设置为 true 或 'multiple',会为每个入口添加一个只含有 runtime 的额外 chunk。
runtime 的 chunk 内容主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有运行时代码。包括已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
optimization: {
// runtimeChunk : true, // 默认名称 runtime~entrypoint.name
runtimeChunk: {
// 根据入口,自定义runtime名称
name: (entrypoint) => `myruntime~${entrypoint.name}`,
}
},
如图所示,就多了2个根据入口文件单独创建的2个webpack连接所有模块化应用程序的运行时js。
总结
- optimization.runtimeChunk 会为每个入口添加一个只含有 runtime 的额外js
import()
ES2020提案 引入的import()
函数,支持动态加载模块。
import()返回一个 Promise 对象,调用 import() 之处,被作为分离的模块起点。
import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
如果您在旧版浏览器(例如 IE 11)中使用 import(),请记住使用诸如 es6-promise 或 promise-polyfill 之类的 polyfill 填充 Promise。
多种使用形式
- 因为返回promise实例,可以搭配Promise.all使用
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
import()也可以用在 async 函数之中。
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
总结
import()主要适用于以下3种情况
(1)按需加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
(2)条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
(3)动态的模块路径,根据函数f的返回结果,加载不同的模块。
import()允许模块路径动态生成。
import(f())
.then(...);
require.ensure
require.ensure() 是 webpack 特有的,已经被 import() 取代。可以作为了解看看,
给定 dependencies 参数,将其对应的文件拆分到一个单独的 bundle 中,此 bundle 会被异步加载。当使用 CommonJS 模块语法时,这是动态加载依赖的唯一方法。意味着,可以在模块执行时才运行代码,只有在满足某些条件时才加载依赖项。
var a = require('normal-dep');
if ( module.hot ) {
require.ensure(['b'], function(require) {
var c = require('c');
// Do something special...
});
}
总结
- require.ensure() 是 webpack 特有的,已经被 import() 取代,作为了解即可。
optimization.splitChunks
webpack4以后,将所有优化编译的配置整合到了optimization选项中,webpack会根据用户配置整合默认配置,调用内部不同的优化插件,进行编译优化。
代码拆分的选项集合到了 optimization.splitChunks 选项中,内部使用的 SplitChunksPlugin 插件。
默认情况下,SplitChunksPlugin 只会影响按需加载的 chunks,即 chunks: "async"
默认配置
optimization:{
splitChunks: {
// 表示分割chunk的类型 可选值有:async,initial和all
chunks: "async",
// 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
minSize: 30000
// 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
minChunks: 1,
// 表示按需加载文件时,并行请求的最大数目。默认为30。
maxAsyncRequests: 30,
// 表示加载入口文件时,并行请求的最大数目。默认为30。
maxInitialRequests: 30,
// 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
automaticNameDelimiter: '~',
// boolean = false string function (module, chunks, cacheGroupKey) => string,也可用于每个cacheGroup: splitChunks.cacheGroups.{cacheGroup}.name。
//拆分块的名称。提供false将保持块的相同名称,因此不会不必要地更改名称。这是生产构建的建议值。
//提供字符串或函数使您可以使用自定义名称。指定字符串或始终返回相同字符串的函数会将所有通用模块和供应商合并为一个块。这可能会导致更大的初始下载量并减慢页面加载速度。
name: false,
// cacheGroups 下可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。
// 一个 module 可能会满足多个 cacheGroups 的正则匹配,最终根据priority来决定打包到哪个组中,数字越大表示优先级越高。
// 默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
cacheGroups: {
svgGroup: {
test(module) {
// test 也可以为一个函数,接受一个module参数,module.resource是资源的绝对路径
const path = require('path');
return (
module.resource &&
module.resource.endsWith('.svg') &&
module.resource.includes(`${path.sep}cacheable_svgs${path.sep}`)
);
},
},
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
},
总结
- splitChunks为webpack提供了更多的代码拆分控制在选项。
- splitChunks配置选项中,我们可以配置更贴合自己项目的性能策略。
建议
关于拆分的一点小意见:
- 路由代码做动态加载:不多说,这个我们一般都是这么做的。
- 分离出的代码太小:如果分离出的代码大小只有几kb,比起多发一个网络请求,倒不如索性打包到一起了。
- 业务和依赖库尽量拆分开:业务代码可能经常上线,每次更改都会改变文件hash,依赖库一般不会变,可以走浏览器缓存。
- 可以按照功能拆分:对于用户经常访问的页面,可以尽可能小的切割,那么多数都都会缓存,不经常访问的,可以采用动态加载。
参考
webpack的optimization.SplitChunks
阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版