代码分离

代码分离

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

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

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

入口起点:使用 entry 配置手动地分离代码。防止重复:使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。动态导入:通过模块的内联函数调用分离代码。入口起点这是迄今为止最简单直观的实现代码分离的方式。不过,这种方式手动配置较多,并有一些隐患。不过,我们将会介绍如何解决这些隐患。先来看看如何从 main bundle 中分离另一个模块:

project

webpack-demo

|- package.json

|- package-lock.json

|- webpack.config.js

|- /dist

|- /src

|- index.js

+ |- another-module.js

|- /node_modulesanother-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正如前面所提及,这种方式存在一些隐患:

如果入口 chunk 之间包含一些重复的模块,那么这些重复模块会被引入到各个 bundle 中。这种方法不够灵活,并且不能动态地拆分应用程序逻辑中的核心代码。以上两点中,第一点所对应的问题已经在我们上面的实例中体现出来了。除了 ./src/another-module.js,我们也曾在 ./src/index.js 中引入过 lodash,这就导致了重复引用。下一章节会介绍如何移除重复的模块。

防止重复入口依赖在配置文件中配置 dependOn 选项,以在多个 chunk 之间共享模块:

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'] }。这样可以获得更好的优化效果,并在使用异步脚本标签时保证执行顺序一致。

SplitChunksPluginSplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件去除之前示例中重复的 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 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了 bundle 大小。执行 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以下是由社区提供,对代码分离很有帮助的 plugin 和 loader:

mini-css-extract-plugin:用于将 CSS 从主应用程序中分离。动态导入webpack 提供了两个类似的技术实现动态代码分离。第一种,也是推荐选择的方式,是使用符合 ECMAScript 提案 的 import() 语法 实现动态导入。第二种则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。让我们先尝试使用第一种。

警告调用 import() 会在内部使用 promise。因此如果在旧版本浏览器中(如 IE 11)使用 import(),需要使用一个 polyfill 库(例如 es6-promise 或 promise-polyfill)shim Promise。

在我们开始之前,先从上述示例的配置中移除多余的 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',

- },

- },

};我们将更新我们的项目,移除现在未使用的文件:

project

webpack-demo

|- package.json

|- package-lock.json

|- webpack.config.js

|- /dist

|- /src

|- index.js

- |- another-module.js

|- /node_modules现在,我们不再静态导入 lodash,而是通过动态导入来分离出一个 chunk:

src/index.js

-import _ from 'lodash';

-

-function component() {

+function getComponent() {

- const element = document.createElement('div');

- // lodash 现在使用 import 引入

- 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 模块时,将不再解析为 module.exports 的值,而是创建一个人工命名空间对象来表示此 CommonJS 模块。参阅 webpack 4: import() and CommonJs 以了解更多有关信息。

试试构建最新的代码,看看 lodash 是否会分离到一个单独的 bundle:

...

[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 函数 一起使用。下面是使用 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);

});提示在稍后示例中,当需要根据计算后的变量导入特定模块时,可以通过向 import() 传入一个 动态表达式 实现。

预获取/预加载模块Webpack v4.6.0+ 增加了对预获取和预加载的支持。

声明 import 时使用下列内置指令可以让 webpack 输出“Resource Hint”告知浏览器:

预获取(prefetch):将来某些导航下可能需要的资源预加载(preload):当前导航下可能需要资源试想一下下面的场景:现在有一个 HomePage 组件,该组件内部渲染了一个 LoginButton 组件,点击后按钮后可以按需加载 LoginModal 组件。

LoginButton.js

//...

import(/* webpackPrefetch: true */ './path/to/LoginModal.js');上面的代码在构建时会生成 并追加到页面头部,指示浏览器在闲置时间预获取 login-modal-chunk.js 文件。

提示只要父 chunk 完成加载,webpack 就会添加预获取提示。

与预获取指令相比,预加载指令有许多不同之处:

预加载 chunk 会在父 chunk 加载时以并行方式开始加载;而预获取 chunk 会在父 chunk 加载结束后开始加载。预加载 chunk 具有中等优先级,并会立即下载;而预获取 chunk 则在浏览器闲置时下载。预加载 chunk 会在父 chunk 中立即请求,用于当下时刻;而预获取 chunk 则用于未来的某个时刻。浏览器支持程度不同。下面这个简单的预加载示例中,有一个 Component,依赖于一个较大的库,所以应该将其分离到一个独立的 chunk 中。

假想这里的图表组件 ChartComponent 组件需要依赖一个体积巨大的 ChartingLibrary 库。它会在渲染时显示一个 LoadingIndicator 组件,然后立即按需导入 ChartingLibrary:

ChartComponent.js

//...

import(/* webpackPreload: true */ 'ChartingLibrary');在页面中使用 ChartComponent 时会在请求 ChartComponent.js 的同时通过 请求 charting-library-chunk。假定 page-chunk 体积比 charting-library-chunk 更小,也更快地被加载完成,那么当 charting-library-chunk 加载完成后,页面会首先显示 LoadingIndicator;当 charting-library-chunk 请求完成后,LoadingIndicator 组件才会消失。这将会使得加载时间能够更短一点,因为只进行单次往返,而不是两次往返,尤其是在高延迟环境下。

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

有时需要自己控制预加载。例如,任何动态导入的预加载都可以通过异步脚本完成。这在流式服务器端渲染的情况下很有用。

const lazyComp = () =>

import('DynamicComponent').catch((error) => {

// 在发生错误时做一些处理

// 例如可以在网络错误的情况下重试请求

});如果在 webpack 开始加载该脚本之前脚本加载失败(如果该脚本不在页面上,webpack 只是创建一个 script 标签来加载其代码),则该 catch 处理程序将不会启动,直到 chunkLoadTimeout 未通过。此行为可能是意料之外的。但这是可以解释的 —— webpack 不能抛出任何错误,因为 webpack 不知道那个脚本失败了。webpack 将在错误发生后立即将 onerror 处理脚本添加到 script 中。

可以通过添加自己的 onerror 处理脚本避免上述问题,这能够帮助在错误发生时移除该脚本

src="https://example.com/dist/dynamicComponent.js"

async

onerror="this.remove()"

>这种情况下错误脚本将被删除,而 webpack 将创建自己的脚本,并且任何错误都将被处理而没有任何超时。

分析 bundle一旦开始分离代码,一件很有帮助的事情是,分析输出结果来检查模块在何处结束。官方分析工具 是一个不错的开始。还有一些其他社区支持的可选项:

webpack-chart:webpack stats 可交互饼图。webpack-visualizer:分析并可视化 bundle,检查哪些模块占用空间,哪些可能是重复使用的。webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。webpack bundle optimize helper:这个工具会分析 bundle,并提供可操作的改进措施,以减少 bundle 的大小。bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。下一步接下来,参阅 懒加载 了解如何在真实的应用程序中使用 import(),以及参阅 缓存 了解如何有效地进行代码分离。

相关推荐

联通迁宽带要多少钱?
beat365手机版客户端ios

联通迁宽带要多少钱?

📅 07-01 👁️ 6149
摩托车自己动手更换刹车油详细教程
beat365手机版客户端ios

摩托车自己动手更换刹车油详细教程

📅 07-03 👁️ 2536
珠江俊景怎么样?看看这份9月的小区报告就知道!
365速发国际平台登陆

珠江俊景怎么样?看看这份9月的小区报告就知道!

📅 07-09 👁️ 1435