简介:代码分割是webpack最大的一个特点,通过这个特点可以将你的代码切割进多个bundles里面,从而从而可以进行按需加载或者并行加载,可以实现更小的bundle和控制资源加载的优先级。
在说代码分割之前,我们先来了解几个概念:
1、bundles:bundles是指webpack打包后文件的统称,单一入口打包出来就只有一个文件,图片,字体文件等其他格式的文件也会被打包进这个bundle文件以base64的形式存在。但是实际开发中我们往往不会这么做,这样会打包出来一个很大的js文件,缺点不必多说,首先是第一次加载时会将所有资源都加载进来,造成首次加载白屏时间过长,第二是无法利用浏览器的并行加载,使加载时间过长,第三是会加载一些可能用不上的资源,造成带宽浪费。
2、chunk:chunk的意思是代码块,一个bundle由一个或者多个chunk组成,一个chunk由一个或多个module组成,chunk是进行代码切割的基础。对于bundle与chunk的之间的关系,webpack打包后的结果拆分成多个代码块,我们称之为chunk,这些chunk是bundles中的一员,因此也可以称之为bundle,就像有一瓶水,我们把它倒进几个杯子,一瓶水就相当于这里的bundles,一杯水就相当于chunk。
3、module:模块,我觉得这与我们项目中架构的模块有所不同,webpack中的模块即文件,每个文件都被看做一个模块。打个比方,假如我们有一个登录模块,里面包含了js,css,img等,在抽象概念中,我们将这些文件抽象地统称为一个模块,即登录模块,而webpack眼中则不同,它是一个文件就是一个模块,即以物理文件的形式划分模块。模块是进行代码分割的最小单位,也就说,我们不能对一个moudle进行更加颗粒化分割,即使这个文件很大。打个比方,现在有一个第三方库,这个库有1M,我们不可能将这个库分割成两个512KB的bundle。当然这得看库的设计者怎么设计打包后的文件了,如果此库被打包成一个文件,我们是无法分割的,比如jquery。如果像是element-ui,库本身对每个组件进行了单独的打包,并不是将所有组件打包进一个文件,所以我们可以进行按需加载。当然对于一些单个较大的文件,我们还有其它优化方式,比如混淆压缩,tree-shaking等。
webpack中代码分割的方式,通常有如下三种:
1、Entry Points:通过配置入口文件来进行分割,这是最简单和最直接的方式,但是这种方式有一定缺点,可能造成代码重复打包,本文讨论的是另一种方式,本文不做讨论。
2、Prevent Duplication: 使用splitChunksPlugin来进行公共代码提取。
3、Dynamic Imports:通过动态代码加载来分割代码,使用import()方法。
调用import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。import方法依赖于Promise,如果需要在低版本浏览器使用,需要进行polyfill。
import的使用
//a.js
import('./b.js').then(b => {
//doSomething
})
import 规范不允许控制模块的名称或其他属性,因为 "chunks" 只是 webpack 中的一个概念,但是我们可以通过注释接收一些特殊的参数,而无须破坏规定:
webpackChunkName:手动指定模块的名称
webpackMode:指定webpack以什么模式解析动态导入
- lazy:默认值,为每个import()导入的模块生成一个可延迟加载的chunk
- lazy-once:生成N个可以延迟加载的chunk,这个模式只能用于部分动态语句中有意义,比如import(`./util/${tool}.js`),webpack会将util下的每个js文件分别打包成一个单独的chunk。
//index.js
let util = 'a'
import(`./util/${util}.js`).then(res => {})
//util/a.js
export function add() {
console.log('add')
}
//util/b.js
export const arr = [1, 2, 3]
打包后的目录结构如下
我们可以看到,除了生成一个主bundle(index.js),还生成了1.js和2.js,分别对应a.js和b.js的chunk。
- eager:这种模式不会生成额外的chunk,所有模块都被当前chunk引入,不会有额外的网络请求。和静态导入相对比,在调用模块之前,该模块不会被执行。(静态导入第一回执行模块代码,ES Module属于静态导入,commonjs的require属于动态导入)
- weak:尝试加载模块,如果该模块函数已经以其他方式加载(即,另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍然会返回 Promise,但是只有在客户端上已经有该 chunk 时才成功解析。如果该模块不可用,Promise 将会是 rejected 状态,并且网络请求永远不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况触发,这对于通用渲染(SSR)是非常有用的。(这个还不没使用过)
注意事项:
- import()不支持完全动态语句,例如import(util),因为webpack 至少需要一些文件的路径信息,而util可能是任何一个路径,webpack不可能将所有模块都打包出来一个chunk。
- 一般我们使用import()动态导入的模块,就不要再在其他模块静态引入此模块了,因为webpack分割chunk时,模块会被打包进静态导入它的chunk中,这样会造成无法分割chunk或者代码重复打包。
//index.js
import('./util/a')
import('./util/b')
//util/a.js
import { arr } from './b'
export function add() {
console.log('add' + arr)
}
//util/b.js
export const arr = [1, 2, 3]
上面代码打包结果:
乍一看,发现并没问题,但是当我们打开打包后的文件,b模块被打包进了1.js,也就是a模块拆分出来的chunk里面
//1.js
;(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
[1, 2],
[
,
function(module, __webpack_exports__, __webpack_require__) {
'use strict'
__webpack_require__.r(__webpack_exports__)
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, 'arr', function() {
return arr
})
const arr = [1, 2, 3]
},
function(module, __webpack_exports__, __webpack_require__) {
'use strict'
__webpack_require__.r(__webpack_exports__)
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, 'add', function() {
return add
})
/* harmony import */
var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1)
function add() {
console.log('add' + _b__WEBPACK_IMPORTED_MODULE_0__['arr'])
}
}
]
])
然后我们查看2.js
//2.js
;(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
[2],
[
,
function(module, __webpack_exports__, __webpack_require__) {
'use strict'
__webpack_require__.r(__webpack_exports__)
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, 'arr', function() {
return arr
})
const arr = [1, 2, 3]
}
]
])
然后我们发现2.js就是b模块,这样b模块就被重复打包了。
再来看另一种情况
//index.js
import { add } from './util/a'
import { arr } from './util/b'
add()
//util/a.js
export function add() {
console.log('add' + arr)
}
import('./b')
//util/b.js
export const arr = [1, 2, 3]
此时打包结果如下,并没有生成额外的chunk。
总结:webpack在处理模块时,从入口文件开始,如果遇到静态导入的模块,则打包进当前chunk,如果遇到动态导入,则判断当前chunk是否已经包含此模块,如果已经包含,则不会生成额外的chunk,如果没有则生成新的chunk。在处理静态导入时,不管有没有此模块的chunk,都会将动态模块打包进当前chunk。
- 关于webpackChunkName,默认使用自增长得chunkId,如果多个模块使用import()动态引入此模块,其chunk名按照深度遍历优先的module命名来,如果最深的那层没有指定,则为第二深的,依次类推,都没有指定则使用自增id。