1. 为什么要分块打包
通过 Webpack 实现前端项目整体模块化的优势固然明显,但是它也会存在一些弊端:它最终会将我们所有的代码打包到一起。试想一下,如果我们的应用非常复杂,模块非常多,那么这种 All in One 的方式就会导致打包的结果过大,甚至超过 4~5M。
在绝大多数的情况下,应用刚开始工作时,并不是所有的模块都是必需的。如果这些模块全部被打包到一起,即便应用只需要一两个模块工作,也必须先把 bundle.js 整体加载进来,而且前端应用一般都是运行在浏览器端,这也就意味着应用的响应速度会受到影响,也会浪费大量的流量和带宽。
所以这种 All in One 的方式并不合理,更为合理的方案是把打包的结果按照一定的规则分离到多个 bundle 中,然后根据应用的运行需要按需加载。这样就可以降低启动成本,提高响应速度。
有人可能认为:Webpack 就是通过把项目中散落的模块打包到一起,从而提高加载效率,那么为什么这里又要分离?这不是自相矛盾吗?
其实这并不矛盾,只是物极必反罢了。Web 应用中的资源受环境所限,太大不行,太碎更不行。因为我们开发过程中划分模块的颗粒度一般都会非常的细,很多时候一个模块只是提供了一个小工具函数,并不能形成一个完整的功能单元。
如果我们不将这些资源模块打包,直接按照开发过程中划分的模块颗粒度进行加载,那么运行一个小小的功能,就需要加载非常多的资源模块。
再者,目前主流的 HTTP 1.1 本身就存在一些缺陷,例如:
- 同一个域名下的并行请求是有限制的;
- 每次请求本身都会有一定的延迟;
- 每次请求除了传输内容,还有额外的请求头,大量请求的情况下,这些请求头加在一起也会浪费流量和带宽。
综上所述,模块打包肯定是必要的,但当应用体积越来越大时,我们也要学会变通。
2. 代码分割
为了解决打包结果过大导致的问题,Webpack 设计了一种分包功能:Code Splitting(代码分割)。
Code Splitting 通过把项目中的资源模块按照我们设计的规则打包到不同的 bundle 中,从而降低应用的启动成本,提高响应速度。
Webpack 实现分包的方式主要有两种:
- 根据业务不同配置多个打包入口,输出多个打包结果;
- 结合 ES Modules 的动态导入(Dynamic Imports)特性,按需加载模块。
2.1 多入口打包
多入口打包一般适用于传统的多页应用程序,最常见的划分规则就是一个页面对应一个打包入口,对于不同页面间公用的部分,再提取到公共的结果中。
实例:
具体结构:
.
├── dist
├── src
│ ├── common
│ │ ├── fetch.js
│ │ └── global.css
│ ├── album.css
│ ├── album.html
│ ├── album.js
│ ├── index.css
│ ├── index.html
│ └── index.js
├── package.json
└── webpack.config.js
这个示例中有两个页面,分别是 index 和 album。代码组织的逻辑也很简单:
- index.js 负责实现 index 页面功能逻辑;
- album.js 负责实现 album 页面功能逻辑;
- global.css 是公用的样式文件;
- fetch.js 是一个公用的模块,负责请求 API。
我们回到配置文件中,这里我们尝试为这个案例配置多入口打包,具体配置如下:
// ./webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js' // [name] 是入口名称
},
// ... 其他配置
plugins: [
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html'
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html'
})
]