webpack 代码分割
什么是代码分割
在最开始使用 Webpack 的时候, 都是将所有的 js 文件全部打包到一个 build.js 文件中(文件名取决与在 webpack.config.js 文件中 output.filename), 但是在大型项目中, build.js 可能过大, 导致页面加载时间过长. 这个时候就需要 code splitting, code splitting 就是将文件分割成块(chunk), 我们可以定义一些分割点(split point), 根据这些分割点对文件进行分块, 并实现按需加载。
代码分割,也就是 Code Splitting 一般需要做这些事情:
为 Vendor 单独打包(Vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存)
为 Manifest(Webpack 的 Runtime 代码)单独打包
为不同入口的业务代码打包,也就是代码分割异步加载(同理,也是为了缓存和加载速度)
为异步公共加载的代码打一个的包
来自蚂蚁金服数据体验技术 Webpack 4 配置最佳实践,这里虽然用的 webpack3,也同样试用。
下面就用 Code Splitting 来实现上面几点。
code splitting
webpack 的代码分割 Code Splitting,主要有两种方式:
第一种:webpack1 通过 require.ensure 定义分割点,将代码进行分割打包,异步加载。第二种:在动态代码拆分方面,webpack 支持符合 ECMAScript 提议的 import()语法进行代码分割和异步加载。
require.ensure 代码分割
webpack 在编译时,会静态地解析代码中的 require.ensure(),同时将模块添加到一个分开的 chunk 当中,这个新的 chunk 会被 webpack 通过 jsonp 来异步加载,其他包则会同步加载。
语法:
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
参数:
第一个参数是 dependencies 依赖列表,webpack 会加载模块,但不会执行。
第二个参数是一个回调,当所有的依赖都加载完成后,webpack 会执行这个回调函数,在其中可以使用 require 导入模块,导入的模块会被代码分割到一个分开的 chunk 中。
第三个参数指定第二个参数中代码分割的 chunkname。
将下面代码拷贝到 webpack.config.RequireEnsure.js:
var webpack = require(‘webpack’);
var path = require(‘path’);
module.exports = {
entry: {
‘pageA’: ‘./src/pageA’,
‘vendor’: [‘lodash’] // 指定单独打包的第三方库(和 CommonsChunkPlugin 结合使用),可以用数组指定多个
},
output: {
path: path.resolve(__dirname, ‘./dist’),
filename: ‘[name].bundle.js’,
chunkFilename: ‘[name].chunk.js’, // code splitting 的 chunk 是异步 (动态) 加载,需要指定 chunkFilename(具体可以了解和 filename 的区别)
},
plugins: [
// 为第三方库和和 manifest(webpack runtime)单独打包
new webpack.optimize.CommonsChunkPlugin({
name: [‘vendor’, ‘manifest’],
minChunks: Infinity
}),
]
}
src/pageA.js、src/subPageA.js、src/subPageB.js、src/module.js 代码如下:
// src/pageA.js
import * as _ from ‘lodash’;
import subPageA from ‘./subPageA’;
import subPageB from ‘./subPageB’;
console.log(‘this is pageA’);
export default ‘pageA’;
// src/subPageA.js
import module from ‘./module’;
console.log(‘this is subPageA’);
export default ‘subPageA’;
// src/subPageB.js
import module from ‘./module’;
console.log(‘this is subPageB’);
export default ‘subPageB’;
// src/module.js
const s = ‘this is module’
export default s;
其中 subPageA 和 subPageB 模块使用共同模块 module.js,命令行运行 webpack –config webpack.config.RequireEnsure.js,打包生成:
pageA.bundle.js
vendor.bundle.js // 为 Vendor 单独打包
manifest.bundle.js // 为 Manifest 单独打包
满足我们文档一刚开始说的代码分割的两点要求,但是我们想要 subPageA 和 subPageB 单独打包:
修改 src/pageA.js,把 import 导入方式改成 require.ensure 的方式就可以代码分割:
// import subPageA from ‘./subPageA’;
// import subPageB from ‘./subPageB’;
require.ensure([], function() {
// 分割./subPageA 模块
var subPageA = require(‘./subPageA’);
}, ‘subPageA’);
require.ensure([], function () {
var subPageB = require(‘./subPageB’);
}, ‘subPageB’);
再次打包,生成:
pageA.bundle.js
subPageA.chunk.js // 代码分割
subPageB.chunk.js
vendor.bundle.js // 为 Vendor 单独打包
manifest.bundle.js // 为 Manifest 单独打包
会发现用了 require.ensure 的模块被代码分割了,达到了我们想要的目的,但是由于 subPageA 和 subPageB 有公共模块 module.js,打开 subPageA.chunk.js 和 subPageB.chunk.js 发现都有公共模块 module.js,这时候就需要在 require.ensure 代码前面加上 require.include(‘./module’)
src/pageA.js:
require.include(‘./module’); // 加在 require.ensure 代码前
再次打包,公共模块 modle.js 被打包在了 pageA.bundle.js,解决了为异步加载的代码打一个公共的包问题。
最后测试一下 webpack 打包后的动态加载 / 异步加载:
在 index.html 里面引入打包文件:
<html>
<body>
<script src=”./dist/manifest.bundle.js”></script>
<script src=”./dist/vendor.bundle.js”></script>
<script src=”./dist/pageA.bundle.js”></script>
</body>
</html>
webpack.config.RequireEnsure.js 加上动态加载路径的配置后再次打包:
output: {
…
publicPath: ‘./dist/’ // 动态加载的路径
}
浏览器中打开 index.html 文件,会发现 subPageA.chunk.js 和 subPageB.chunk.js 没有引入也被导入了进来。其实这两个文件是 webpack runtime 打包文件根据代码分割的文件自动异步加载的。
<img src=”https://user-gold-cdn.xitu.io…;h=115&f=png&s=12335″>
dynamic import 代码分割
requre.ensure 好像已被 webpack4 废弃。es6 提供了一种更好的代码分割方案也就是 dynamic import(动态加载)的方式,webpack 打包时会根据 import()自动代码分割;虽然在提案中,可以安装 babel 插件兼容,推荐用这种方式:
语法:
import(/* webpackChunkName: chunkName */ chunk)
.then(res => {
// handle something
})
.catch(err => {
// handle err
});
其中 / chunkName / 为指定代码分割包名,chunk 指定需要代码分割的文件入口。注意不要把 import 关键字和 import()方法弄混了,该方法是为了进行动态加载。
import()调用内部使用 promises。如果您使用 import()较旧的浏览器,请记住 Promise 使用诸如 es6-promise 或 promise-polyfill 之类的 polyfill 进行填充。
下面修改 pageA.js:
import * as _ from ‘lodash’;
import(/* webpackChunkName: ‘subPageA’ */’./subPageA’).then(function(res){
console.log(‘import()’, res)
})
import(/* webpackChunkName: ‘subPageB’ */’./subPageB’).then(function(res){
console.log(‘import()’, res)
})
console.log(‘this is pageA’);
export default ‘pageA’;
将下面代码拷贝到 webpack.config.import.js:
var webpack = require(‘webpack’);
var path = require(‘path’);
module.exports = {
entry: {
‘pageA’: ‘./src/pageA’,
‘vendor’: [‘lodash’] // 指定单独打包的第三方库(和 CommonsChunkPlugin 结合使用),可以用数组指定多个
},
output: {
path: path.resolve(__dirname, ‘./dist’),
filename: ‘[name].bundle.js’,
chunkFilename: ‘[name].chunk.js’, // code splitting 的 chunk 是异步 (动态) 加载,需要指定 chunkFilename(具体可以了解和 filename 的区别)
publicPath: ‘./dist/’ // 动态加载的路径
},
plugins: [
// 为第三方库和和 manifest(webpack runtime)单独打包
new webpack.optimize.CommonsChunkPlugin({
name: [‘vendor’, ‘manifest’],
minChunks: Infinity
}),
]
}
命令行 webpack –config webpack.config.import.js,打包发现和 require.ensure 一样的结果:
pageA.bundle.js
subPageA.chunk.js // 代码分割
subPageB.chunk.js
vendor.bundle.js // 为 Vendor 单独打包
manifest.bundle.js // 为 Manifest 单独打包
为了分离出 subPageA.chunk.js 和 subPageB.chunk.js 的公共模块 module.js,可以用 CommonsChunkPlugin 的 async 方式给异步加载的打包提取公共模块:
在 webpack.config.import.js 加上下面配置:(其实这种方式可以替代 require.ensure 中的 require.include 方式提取公共代码,更自动和简单)
plugins: [
// 为异步公共加载的代码打一个的包
new webpack.optimize.CommonsChunkPlugin({
async: ‘async-common’, // 异步公共的代码
children: true, // 要加上 children,会从入口的子依赖开始找
minChunks: 2 // 出现 2 次或以上相同代码就打包
}),
…
]
重新打包,打包文件如下:
pageA.bundle.js
subPageA.chunk.js // 代码分割
subPageB.chunk.js
async-common-pageA.chunk.js // 为异步公共加载的代码打的包
vendor.bundle.js // 为 Vendor 单独打包
manifest.bundle.js // 为 Manifest 单独打包
会发现多了一个异步加载包 subPageA.chunk.js 和 subPageB.chunk.js 的公共模块 async-common-pageA.chunk.js 包,配置成功!
同样,还是进行测试,index.html 引入同步加载的包:
<html>
<body>
<script src=”./dist/manifest.bundle.js”></script>
<script src=”./dist/vendor.bundle.js”></script>
<script src=”./dist/pageA.bundle.js”></script>
</body>
</html>
浏览器中打开 index.html 文件,会发现 subPageA.chunk.js、subPageB.chunk.js 和 async-common-pageA.chunk.js 被自动异步加载了。
<img src=”https://user-gold-cdn.xitu.io…;h=136&f=png&s=15038″>
关于异步加载和按需加载
关于文档中多次提到 webpack 代码分割的包,在浏览器中 webpack runtime 的包会自动异步加载代码分割的包,那么在 react 和 vue 应用中,如果这些代码分割包在页面初始化也会自动异步加载,那不是分包的作用不大?原因其实是我们上面例子执行了 import()或 require.ensure,而在应用中,写法是当请求路由的时候才执行 import()或 require.ensure,然后再异步加载,webpack 遇到 import()或 require.ensure 的配置的时候只会进行代码切割,这种思路就是按需加载的基础。
部分参考链接:
代码分割 – 使用 require.ensure
文档中出现的源代码在我的 github,感兴趣的欢迎 star。