代码分离

本指南继续沿用 起步管理输出 中的代码示例。请确保你已熟悉这些指南中提供的示例。

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

常用的代码分离方法有三种:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块中的内联函数调用来分离代码。

入口起点(entry points)

这是迄今为止最简单、最直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。先来看看如何从 main bundle 中分离 another module(另一个模块):

project

  1. webpack-demo
  2. |- package.json
  3. |- webpack.config.js
  4. |- /dist
  5. |- /src
  6. |- index.js
  7. + |- another-module.js
  8. |- /node_modules

another-module.js

  1. import _ from 'lodash';
  2. console.log(
  3. _.join(['Another', 'module', 'loaded!'], ' ')
  4. );

webpack.config.js

  1. const path = require('path');
  2. module.exports = {
  3. mode: 'development',
  4. entry: {
  5. index: './src/index.js',
  6. + another: './src/another-module.js'
  7. },
  8. output: {
  9. filename: '[name].bundle.js',
  10. path: path.resolve(__dirname, 'dist')
  11. }
  12. };

这将生成如下构建结果:

  1. ...
  2. Asset Size Chunks Chunk Names
  3. another.bundle.js 550 KiB another [emitted] another
  4. index.bundle.js 550 KiB index [emitted] index
  5. Entrypoint index = index.bundle.js
  6. Entrypoint another = another.bundle.js
  7. ...

正如前面提到的,这种方式存在一些隐患:

  • 如果入口 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');
  2. module.exports = {
  3. mode: 'development',
  4. entry: {
  5. index: './src/index.js',
  6. another: './src/another-module.js'
  7. },
  8. output: {
  9. filename: '[name].bundle.js',
  10. path: path.resolve(__dirname, 'dist')
  11. },
  12. + optimization: {
  13. + splitChunks: {
  14. + chunks: 'all'
  15. + }
  16. + }
  17. };

使用 optimization.splitChunks 配置选项,现在可以看到已经从 index.bundle.jsanother.bundle.js 中删除了重复的依赖项。需要注意的是,此插件将 lodash 这个沉重负担从主 bundle 中移除,然后分离到一个单独的 chunk 中。执行 npm run build 查看效果:

  1. ...
  2. Asset Size Chunks Chunk Names
  3. another.bundle.js 5.95 KiB another [emitted] another
  4. index.bundle.js 5.89 KiB index [emitted] index
  5. vendors~another~index.bundle.js 547 KiB vendors~another~index [emitted] vendors~another~index
  6. Entrypoint index = vendors~another~index.bundle.js index.bundle.js
  7. Entrypoint another = vendors~another~index.bundle.js another.bundle.js
  8. ...

以下是由社区提供,一些对于代码分离很有帮助的 plugin 和 loader:

动态导入(dynamic imports)

当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。让我们先尝试使用第一种……

import() 调用会在内部用到 promises。如果在旧版本浏览器中使用 import(),记得使用一个 polyfill 库(例如 es6-promisepromise-polyfill),来 shim Promise

在开始之前,我们先从配置中移除掉多余的 entryoptimization.splitChunks,因为接下来的演示中并不需要它们:

webpack.config.js

  1. const path = require('path');
  2. module.exports = {
  3. mode: 'development',
  4. entry: {
  5. + index: './src/index.js'
  6. - index: './src/index.js',
  7. - another: './src/another-module.js'
  8. },
  9. output: {
  10. filename: '[name].bundle.js',
  11. + chunkFilename: '[name].bundle.js',
  12. path: path.resolve(__dirname, 'dist')
  13. },
  14. - optimization: {
  15. - splitChunks: {
  16. - chunks: 'all'
  17. - }
  18. - }
  19. };

注意,这里使用了 chunkFilename,它决定 non-entry chunk(非入口 chunk) 的名称。关于 chunkFilename 更多信息,请查看 输出 文档。更新我们的项目,移除现在不会用到的文件:

project

  1. webpack-demo
  2. |- package.json
  3. |- webpack.config.js
  4. |- /dist
  5. |- /src
  6. |- index.js
  7. - |- another-module.js
  8. |- /node_modules

现在,我们不再使用 statically import(静态导入) lodash,而是通过 dynamic import(动态导入) 来分离出一个 chunk:

src/index.js

  1. - import _ from 'lodash';
  2. -
  3. - function component() {
  4. + function getComponent() {
  5. - var element = document.createElement('div');
  6. -
  7. - // Lodash, now imported by this script
  8. - element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  9. + return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
  10. + var element = document.createElement('div');
  11. +
  12. + element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  13. +
  14. + return element;
  15. +
  16. + }).catch(error => 'An error occurred while loading the component');
  17. }
  18. - document.body.appendChild(component());
  19. + getComponent().then(component => {
  20. + document.body.appendChild(component);
  21. + })

这里我们需要使用 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. ...
  2. Asset Size Chunks Chunk Names
  3. index.bundle.js 7.88 KiB index [emitted] index
  4. vendors~lodash.bundle.js 547 KiB vendors~lodash [emitted] vendors~lodash
  5. Entrypoint index = index.bundle.js
  6. ...

由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。但是,需要使用像 Babel 这样的预处理器和 Syntax Dynamic Import Babel Plugin。下面是如何通过 async 函数简化代码:

src/index.js

  1. - function getComponent() {
  2. + async function getComponent() {
  3. - return import(/* webpackChunkName: "lodash" */ 'lodash').then({ default: _ } => {
  4. - var element = document.createElement('div');
  5. -
  6. - element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  7. -
  8. - return element;
  9. -
  10. - }).catch(error => 'An error occurred while loading the component');
  11. + var element = document.createElement('div');
  12. + const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
  13. +
  14. + element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  15. +
  16. + return element;
  17. }
  18. getComponent().then(component => {
  19. document.body.appendChild(component);
  20. });

预取/预加载模块(prefetch/preload module)

webpack v4.6.0+ 添加了预取和预加载的支持。

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:

  • prefetch(预取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。

LoginButton.js

  1. //...
  2. import(/* webpackPrefetch: true */ 'LoginModal');

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部,指示着浏览器在闲置时间预取 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. //...
  2. import(/* webpackPreload: true */ 'ChartingLibrary');

在页面中使用 ChartComponent 时,在请求 ChartComponent.js 的同时,还会通过 <link rel="preload"> 请求 charting-library-chunk。假定 page-chunk 体积很小,很快就被加载好,页面此时就会显示 LoadingIndicator(加载进度条) ,等到 charting-library-chunk 请求完成,LoadingIndicator 组件才消失。启动仅需要很少的加载时间,因为只进行单次往返,而不是两次往返。尤其是在高延迟环境下。

不正确地使用 webpackPreload 会有损性能,请谨慎使用。

bundle 分析(bundle analysis)

如果我们以分离代码作为开始,那么就应该以检查模块的输出结果作为结束,对其进行分析是很有用处的。官方提供分析工具 是一个好的初始选择。下面是一些可选择的社区支持(community-supported)工具:

  • webpack-chart:webpack stats 可交互饼图。
  • webpack-visualizer:可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为便捷的、交互式、可缩放的树状图形式。
  • webpack bundle optimize helper:此工具会分析你的 bundle,并为你提供可操作的改进措施建议,以减少 bundle 体积大小。

下一步

接下来,查看 延迟加载 来学习如何在实际一个真实应用程序中使用 import() 的具体示例,以及查看 缓存 来学习如何有效地分离代码。


进一步阅读


贡献人员

EugeneHlushko EugeneHlushko TheDutchCoder TheDutchCoder Tiendo1011 Tiendo1011 bartushek bartushek byzyk byzyk chrisVillanueva chrisVillanueva efreitasn efreitasn jakearchibald jakearchibald johnstew johnstew jonwheeler jonwheeler kcolton kcolton levy9527 levy9527 pastelsky pastelsky pksjce pksjce rafde rafde rahulcs rahulcs rouzbeh84 rouzbeh84 shaodahong shaodahong shaunwallace shaunwallace shinxi shinxi simon04 simon04 skipjack skipjack sudarsangp sudarsangp tomtasche tomtasche