Webpack Code Splitting
这几天在看webpack的教学视频时对代码分离产生了一点疑惑,于是去翻看了官方文档,现在将原文档的翻译以及我对其中一些内容的理解整理下来。
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
常用的代码分离方法有三种:
- 入口起点:使用
entry
配置手动地分离代码。 - 防止重复:使用
SplitChunksPlugin
去重和分离 chunk。 - 动态导入:通过模块中的内联函数调用来分离代码。
首先这里要解释一下bundle与chunk的区别与联系:
具体的解释可以看这里,但是总结来讲就是:
- 模块就是模块可以是ESM模块也可以是commonJS或者AMD模块
- 打包过程中被操作的模块文件叫做chunk,例如异步加载一个模块就是一个chunk
- bundle是最后打包后的文件,最终文件可以和chunk长的一模一样,但是大部分情况下他是多个chunk的集合
入口起点(entry points)
这是迄今为止最简单、最直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。先来看看如何从 main bundle 中分离 another module(另一个模块):
project
1 | webpack-demo |
another-module.js
1 | import _ from 'lodash'; |
webpack.config.js
1 | const path = require('path'); |
关于上述配置文件中的 output.filename 中的 [name],它的作用是可以让打包生成的文件名称是入口[文件名称.bundle.js],这里还可以配置许多其他的配置,详细的配置可以看官方文档。
这将生成如下构建结果:
1 | ... |
正如前面提到的,这种方式存在一些隐患:
- 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。因为这里会把根据每个入口文件产生的依赖关系中的所有文件都打包到各自的同一个文件中。
- 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。
这两点中的第一点,对我们的示例来说毫无疑问是个严重问题,因为我们在 ./src/index.js
中也引入过 lodash
,这样就造成在两个 bundle 中重复引用。我们可以通过使用 SplitChunksPlugin
插件来移除重复模块。
防止重复(prevent duplication)
SplitChunksPlugin
插件可以将公共的依赖模块提取到已有的 entry chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将前面示例中重复的 lodash
模块去除:
CommonsChunkPlugin
已经从 webpack v4(代号 legato)中移除。想要了解最新版本是如何处理 chunk,请查看SplitChunksPlugin
。
webpack.config.js
1 | const path = require('path'); |
使用 optimization.splitChunks
配置选项,现在可以看到已经从 index.bundle.js
和 another.bundle.js
中删除了重复的依赖项。需要注意的是,此插件将 lodash
这个沉重负担从主 bundle 中移除,然后分离到一个单独的 chunk 中。执行 npm run build
查看效果:
1 | ... |
以下是由社区提供,一些对于代码分离很有帮助的 plugin 和 loader:
mini-css-extract-plugin
:用于将 CSS 从主应用程序中分离。bundle-loader
:用于分离代码和延迟加载生成的 bundle。promise-loader
:类似于bundle-loader
,但是使用了 promise API。
动态导入(dynamic imports)
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import()
语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure
。让我们先尝试使用第一种……
import()
调用会在内部用到 promises。如果在旧版本浏览器中使用import()
,记得使用一个 polyfill 库(例如 es6-promise 或 promise-polyfill),来 shimPromise
。
在开始之前,我们先从配置中移除掉多余的 entry
和 optimization.splitChunks
,因为接下来的演示中并不需要它们:
webpack.config.js
1 | const path = require('path'); |
注意,这里使用了 chunkFilename
,它决定 non-entry chunk(非入口 chunk) 的名称。关于 chunkFilename
更多信息,请查看 输出 文档。更新我们的项目,移除现在不会用到的文件:
project
1 | webpack-demo |
现在,我们不再使用 statically import(静态导入) lodash
,而是通过 dynamic import(动态导入) 来分离出一个 chunk:
src/index.js
1 | - import _ from 'lodash'; |
这里我们需要使用 default
的原因是,从 webpack v4 开始,在 import CommonJS 模块时,不会再将导入模块解析为 module.exports
的值,而是为 CommonJS 模块创建一个 artificial namespace object(人工命名空间对象),关于其背后原因的更多信息,请阅读 webpack 4: import() 和 CommonJs。
注意,在注释中我们提供了 webpackChunkName
。这样会将拆分出来的 bundle 命名为 lodash.bundle.js
,而不是 [id].bundle.js
。想了解更多关于 webpackChunkName
和其他可用选项,请查看 import()
文档。让我们执行 webpack,看到 lodash
分离出一个单独的 bundle:
1 | ... |
由于 import()
会返回一个 promise,因此它可以和 async
函数一起使用。但是,需要使用像 Babel 这样的预处理器和 Syntax Dynamic Import Babel Plugin。下面是如何通过 async 函数简化代码:
src/index.js
1 | - function getComponent() { |
预取/预加载模块(prefetch/preload module)
webpack v4.6.0+ 添加了预取和预加载的支持。
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:
- prefetch(预取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
下面这个 prefetch 的简单示例中,有一个 HomePage
组件,其内部渲染一个 LoginButton
组件,然后在点击后按需加载 LoginModal
组件。
LoginButton.js
1 | //... |
这会生成 `` 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js
文件。
只要父 chunk 完成加载,webpack 就会添加 prefetch hint(预取提示)。
与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
- 浏览器支持程度不同。
下面这个简单的 preload 示例中,有一个 Component
,依赖于一个较大的 library,所以应该将其分离到一个独立的 chunk 中。
我们假想这里的图表组件 ChartComponent
组件需要依赖体积巨大的 ChartingLibrary
库。它会在渲染时显示一个 LoadingIndicator(加载进度条)
组件,然后立即按需导入 ChartingLibrary
:
ChartComponent.js
1 | //... |
在页面中使用 ChartComponent
时,在请求 ChartComponent.js 的同时,还会通过 `` 请求 charting-library-chunk。假定 page-chunk 体积很小,很快就被加载好,页面此时就会显示 LoadingIndicator(加载进度条)
,等到 charting-library-chunk
请求完成,LoadingIndicator 组件才消失。启动仅需要很少的加载时间,因为只进行单次往返,而不是两次往返。尤其是在高延迟环境下。
不正确地使用 webpackPreload 会有损性能,请谨慎使用。
对于其他webpack支持的打包方法可以去看这篇文档