代码分割是 webpack 最引人注目的特性之一。此特性允许您将代码分割成各种包,然后按需或并行加载。它可用于实现更小的包并控制资源加载优先级,如果使用得当,这可能会对加载时间产生重大影响。
代码分割有三种通用方法:
entry
配置手动分割代码。SplitChunksPlugin
来去重和分割代码块。这是迄今为止分割代码最简单、最直观的方法。然而,它更偏向手动操作,并且存在一些我们将要讨论的缺陷。让我们看看如何将另一个模块从主包中分割出来:
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules
another-module.js
import _ from 'lodash';
console.log(_.join(['Another', 'module', 'loaded!'], ' '));
webpack.config.js
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ mode: 'development',
+ entry: {
+ index: './src/index.js',
+ another: './src/another-module.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
这将产生以下构建结果:
...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms
如前所述,这种方法存在一些缺陷:
这两个问题中的第一个对我们的示例来说无疑是个问题,因为 lodash
也被导入到 ./src/index.js
中,因此会在两个包中重复。让我们在下一节中消除这种重复。
dependOn
选项允许在代码块之间共享模块。
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
如果要在单个 HTML 页面上使用多个入口点,还需要 optimization.runtimeChunk: 'single'
,否则我们可能会遇到此处描述的问题。
webpack.config.js
const path = require('path');
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',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
这是构建结果:
...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms
如您所见,除了 shared.bundle.js
、index.bundle.js
和 another.bundle.js
之外,还生成了另一个 runtime.bundle.js
文件。
尽管 webpack 允许每个页面使用多个入口点,但应尽可能避免,转而使用带有多重导入的单个入口点:entry: { page: ['./analytics', './app'] }
。这在使用 async
脚本标签时,能带来更好的优化和一致的执行顺序。
SplitChunksPlugin
允许我们将公共依赖项提取到现有入口块或全新的块中。让我们用它来消除前面示例中 lodash
依赖的重复。
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
配置了 optimization.splitChunks
选项后,我们现在应该会看到 index.bundle.js
和 another.bundle.js
中重复的依赖项已被移除。该插件应该会注意到我们已将 lodash
分割到一个单独的代码块中,并从主包中移除了多余的部分。然而,重要的是要注意,只有当公共依赖项满足 webpack 指定的大小阈值时,它们才会被提取到单独的代码块中。
让我们运行 npm run build
看看它是否奏效:
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms
以下是一些社区提供的其他有用的代码分割插件和加载器:
mini-css-extract-plugin
: 有助于将 CSS 从主应用程序中分割出来。webpack 在动态代码分割方面支持两种类似的技术。第一种也是推荐的方法是使用符合 ECMAScript 动态导入提案的 import()
语法。遗留的、webpack 特有的方法是使用 require.ensure
。让我们尝试使用这两种方法中的第一种…
在开始之前,让我们从上面示例的配置中移除多余的 entry
和 optimization.splitChunks
,因为在接下来的演示中它们将不再需要。
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
- another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- optimization: {
- splitChunks: {
- chunks: 'all',
- },
- },
};
我们还将更新项目以删除现在未使用的文件。
项目
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules
现在,我们将使用动态导入而不是静态导入 lodash
来分离一个代码块。
src/index.js
-import _ from 'lodash';
-
-function component() {
+function getComponent() {
- const element = document.createElement('div');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ return import('lodash')
+ .then(({ default: _ }) => {
+ const element = document.createElement('div');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- return element;
+ return element;
+ })
+ .catch((error) => 'An error occurred while loading the component');
}
-document.body.appendChild(component());
+getComponent().then((component) => {
+ document.body.appendChild(component);
+});
我们需要 default
的原因在于,自 webpack 4 起,当导入 CommonJS 模块时,import 不再解析为 module.exports
的值,而是为 CommonJS 模块创建一个人工命名空间对象。有关此原因的更多信息,请阅读 webpack 4: import() and CommonJs。
让我们运行 webpack 看看 lodash
是否被分割到了一个单独的包中。
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
./src/index.js 434 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms
由于 import()
返回一个 Promise,它可以与 async
函数一起使用。以下是它如何简化代码的示例:
src/index.js
-function getComponent() {
+async function getComponent() {
+ const element = document.createElement('div');
+ const { default: _ } = await import('lodash');
- return import('lodash')
- .then(({ default: _ }) => {
- const element = document.createElement('div');
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- return element;
- })
- .catch((error) => 'An error occurred while loading the component');
+ return element;
}
getComponent().then((component) => {
document.body.appendChild(component);
});
Webpack 4.6.0+ 增加了对预取和预加载的支持。
在声明导入时使用这些内联指令,webpack 能够输出“资源提示”,它告诉浏览器:
一个示例是拥有一个 HomePage
组件,它渲染一个 LoginButton
组件,然后 LoginButton
被点击后按需加载一个 LoginModal
组件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
这将导致 <link rel="prefetch" href="login-modal-chunk.js">
被添加到页面的
login-modal-chunk.js
文件。
预加载指令与预取指令相比有许多不同之处:
一个例子是拥有一个组件,该组件总是依赖于一个应该在单独代码块中的大型库。
让我们想象一个 ChartComponent
组件,它需要一个庞大的 ChartingLibrary
。它在渲染时显示一个 LoadingIndicator
,并立即按需导入 ChartingLibrary
。
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
当请求使用 ChartComponent
的页面时,charting-library-chunk
也通过 <link rel="preload">
被请求。假设 page-chunk 更小且完成更快,页面将显示一个 LoadingIndicator
,直到已经请求的 charting-library-chunk
完成。这将稍微提高加载时间,因为它只需要一次往返而不是两次。特别是在高延迟环境中。
有时您需要对预加载进行自己的控制。例如,任何动态导入的预加载都可以通过异步脚本完成。这在流式服务器端渲染的情况下非常有用。
const lazyComp = () =>
import('DynamicComponent').catch((error) => {
// Do something with the error.
// For example, we can retry the request in case of any net error
});
如果脚本在 webpack 自己开始加载该脚本之前失败(如果该脚本不在页面上,webpack 会创建一个脚本标签来加载其代码),那么该 catch 处理程序在 chunkLoadTimeout
超时之前不会启动。这种行为可能出乎意料。但这是可以解释的——webpack 无法抛出任何错误,因为 webpack 不知道脚本失败了。webpack 会在错误发生后立即向脚本添加 onerror 处理程序。
为了防止此类问题,您可以添加自己的 onerror 处理程序,以便在发生任何错误时删除脚本。
<script
src="https://example.com/dist/dynamicComponent.js"
async
onerror="this.remove()"
></script>
在这种情况下,出错的脚本将被移除。Webpack 将创建自己的脚本,并且任何错误都将立即处理,无需任何超时。
一旦您开始分割代码,分析输出以检查模块的最终位置会很有用。官方的分析工具是一个很好的起点。还有一些其他社区支持的选项: