Webpack 能够把代码分离到不同的 bundle(打包分离出来的文件) 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大优化加载时间。
常用的代码分离方法有三种:
- 入口起点:使用
entry配置项手动地分离代码; - 防止重复:使用
Entry dependencies或者SplitChunksPlugin去重和分离 chunk; - 动态导入:通过模块的内联函数调用来分离代码。
一、入口起点
入口起点:使用 entry 配置项配置多个入口手动地分离代码,这样就会对多个入口文件分别打包,在 dist 目录生成多个对应的 bundle,从而实现代码分离。
这种方式存在的问题:
- 如果入口 chunk 之间存在一些重复的模块,那些重复模块会被重复打包并引入到各个 bundle 中;
- 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。
在 src 目录下创建 another-module.js:
import _ from 'lodash'
console.log(_.join(['Another', 'module', 'loaded!'], ' '))
这个模块依赖了 lodash,需要安装 lodash:
npm install lodash --save-dev
webpack.config.js:
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js', // 多个入口,不能只配置一个出口文件
},
}
打包后的结果:
上图中,只有两行代码的 another-module.js 打包后的体积居然有 554k,这是因为 another-module.js 中引入了 lodash,打包的时候 lodash.js 也被打包进去了。
再来修改一下 index.js,在 index.js 中也引入 lodash:
import helloWorld from './hello-world.js'
import _ from 'lodash'
helloWorld()
console.log(_.join(['index', 'module', 'loaded!'], ' '))
打包后的结果:
上图中,对比修改前与修改后的 index.js 打包后的 index.bundle.js,体积明显增大了很多,这是因为引入了 lodash,第二次打包把 lodash 也打包进去了。
二、防止重复
防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk,将公共的文件抽离成单独的 chunk。
1. 入口依赖(Entry dependencies)
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: 'shared'
},
another: {
import: './src/another-module.js',
dependOn: 'shared'
},
shared: 'lodash'
},
output: {
filename: '[name].bundle.js', // 多个入口,不能只配置一个出口文件
},
}
从上图可以看出,index.bundle.js 与 another.bundle.js 共享的模块 lodash.js 被打包到一个单独的文件 shared.bundle.js 中。
2. SplitChunksPlugin
SplitChunksPlugin 是一个内置插件,可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js', // 多个入口,不能只配置一个出口文件
},
optimization: {
splitChunks: {
chunks: 'all',
},
}
}
使用 optimization.splitChunks 配置选项之后,index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块,且 lodash 被分离到单独的 chunk。
三、动态导入
动态导入:通过模块的内联函数调用来分离代码。
当涉及到动态代码拆分时,Webpack 提供了两个类似的技术:
- 使用
import()语法来实现动态导入(推荐) - 使用 Webpack 特定的
require.ensure(遗留功能)
1. import() 的基本使用
在 async-module.js 中使用 import() 语法 引入 lodash:
function getComponent() {
return import('lodash') // 使用 import 动态引入 lodash
.then(({default: _}) => {
const element = document.createElement('div')
element.innerHTML = _.join(['Hello', 'webpack'], ' ')
return element
})
}
getComponent().then((element) => {
document.body.appendChild(element)
})
入口文件 index.js:
import helloWorld from './hello-world.js'
import _ from 'lodash' // index.js 中引入了 lodash(第一处)
import './async-module.js' // async-module.js 中引入了 lodash(第二处)
helloWorld()
console.log(_.join(['index', 'module', 'loaded!'], ' '))
webpack.config.js:
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js' // another-module.js 中引入了 lodash(第三处)
},
output: {
filename: '[name].bundle.js', // 多个入口,不能只配置一个出口文件
},
optimization: {
splitChunks: {
chunks: 'all',
},
}
}
以上代码,有 3 个地方都引入了 lodash,但只打包出了一个 lodash 文件:
2. import() 的应用:懒加载
懒加载或者按需加载,是一种很好的优化方式。这种方式,实际上是先把代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,再引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
入口文件 index.js:
const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', () => {
import(/* webpackChunkName: 'math' */'./math.js').then(({ add }) => {
console.log(add(4, 5))
})
})
document.body.appendChild(button)
以上代码:
- 其中的注释,称为 Webpack 魔法注释:
webpackChunkName: 'math'告诉 Webpack 打包生成的文件名为 math; - 由于使用了
import()动态导入,所以会在 dist 目录中生成一个单独的文件 math.bundle.js; - 第一次加载完页面时,dist 目录中的 math.bundle.js 不会被加载,只有当点击按钮后,才会加载 math.bundle.js 文件。
3. import() 的应用:预获取/预加载模块
Webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,可以让 Webpack 输出 "resource hint(资源提示)",来告知浏览器:
prefetch(预获取):将来某些导航下可能需要的资源;preload(预加载):当前导航下可能需要的资源。
3.1 预获取(prefetch)
入口文件 index.js:
const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', () => {
import(/* webpackChunkName: 'math', webpackPrefetch: true */'./math.js').then(({ add }) => {
console.log(add(4, 5))
})
})
document.body.appendChild(button)
魔法注释中的 webpackPrefetch: true 告诉 Webpack 执行预获取,这会生成 <link rel="prefetch" href="math.js"> 并追加到页面头部,指示浏览器在闲置时加载 math.js 文件。
如图,当页面加载完成,浏览器空闲时就会加载 math.bundle.js:
3.2 预加载(preload)
入口文件 index.js:
const button2 = document.createElement('button')
button2.textContent = '点击执行字符串打印'
button2.addEventListener('click', () => {
import(/* webpackChunkName: 'print', webpackPreload: true */ './print.js').then(({ print }) => {
print(4, 5)
})
})
document.body.appendChild(button2)
魔法注释中的 webpackPreload: true 告诉 Webpack 执行预加载。
以上代码,虽然给 print.js 设置了预加载,但也是要等到点击了按钮才会预加载,不会提前加载。
如下图,没有点击按钮时,浏览器不会加载 print.bundle.js:
如果需要提前加载,可以改成下面这样(直接 import):
import(/* webpackChunkName: 'print', webpackPreload: true */ './print.js').then(({ print }) => {
print(4, 5)
})
加载页面时,print.bundle.js 就预加载了:
3.3 预获取(prefetch)与预加载(preload)的区别
- 加载时机:preload chunk 会在父 chunk 加载时,以并行方式立即加载;prefetch chunk 会在父 chunk 加载结束后浏览器闲置时加载。
- 使用时机:preload chunk 会在父 chunk 中立即请求,用于当下时刻;prefetch chunk 会用于未来的某个时刻。
- 浏览器支持程度不同。