随着功能的增加,Web 应用也越来越大。应用程序加载所需的时间越长,对用户来说就越令人沮丧。在连接速度较慢的移动环境中,该问题还会被放大。
即使拆分打包可以帮助您提升一个档次,但它们并不能完全解决问题,您仍然需要下载大量数据。幸运的是,由于代码拆分,可以更好地解决这个问题。它可以根据您的需要延迟加载代码。
您可以在用户进入应用程序的新视图时加载需要的代码。您还可以将加载绑定到特定操作,例如滚动或单击按钮。您还可以尝试预测用户下一步要做什么,并根据您的猜测加载代码。这样,当用户尝试访问它时,功能就已存在。
顺便说一下,使用 Webpack 的延迟加载可以实现 Google 的 PRPL模式。PRPL(推送,渲染,预缓存,延迟加载)的设计考虑了移动网络。
代码拆分格式
我们可以在 Webpack 中以两种主要方式完成代码拆分:通过动态 import
或 require.ensure
语法。在本书的项目中,我们使用前者。
我们的最终目标是得到一个按需加载的分割点。分割内部也可以再次分割,您可以根据分割构建整个应用程序。这样做的好处是,应用程序的初始有效负载会更小。
动态 import
动态 import
语法 目前还不是官方语言规范。由于这个原因,我们需要在 Babel 设置中进行一些小的调整。
动态导入被定义为 Promise
:
import(/* webpackChunkName: "optional-name" */ "./module").then(
module => {...}
).catch(
error => {...}
);
optional-name
允许您将多个拆分点打包成单个包。只要它们具有相同的名称,它们就会被分组到一起。每个拆分点默认生成一个单独的包。
promise 是可以组合的,因此您可以并行加载多个资源:
Promise.all([
import("lunr"),
import("../search_index.json"),
]).then(([lunr, search]) => {
return {
index: lunr.Index.load(search.index),
lines: search.lines,
};
});
上面的代码并行请求两个包。如果你只希望发出一个请求,则必须使用 option name
或定义中间模块来合并 import
。
在以正确的方式配置之后,改语法仅适用于 JavaScript。如果您使用其他环境,则可能必须使用以下各节中介绍的替代方案。
有一个较旧的语法,require.ensure。实际上,新语法可以涵盖相同的功能。另请参见 require.include。
webpack-pwa 在更大范围内阐述了代码分割,并讨论了不同的基于 shell 的方法。您将在“多页”一章了解这一主题。
设置代码拆分
为了实现代码拆分,您可以使用动态 import
;另外,还需要设置 Babel 以使语法有效。
配置 Babel
Babel 本身不支持动态 import
语法,它需要 @babel/plugin-syntax-dynamic-import 配合才能工作。
首先安装它:
npm install @babel/plugin-syntax-dynamic-import --save-dev
要将其引入到项目中,请按如下方式调整配置:
.babelrc
{
"plugins": ["@babel/plugin-syntax-dynamic-import"],
...
}
如果您使用的是 ESLint,你需要安装
babel-eslint
,并且在 ESLint 中设置parser: "babel-eslint"
,此外,你还要设置parserOptions.allowImportExportEverywhere: true
使用动态 import 定义分割点
下面我们通过一个简单的例子来演示:
src/lazy.js
export default "Hello from lazy";
您还需组件中引用这个文件,每当用户碰巧点击按钮时,您都会触发加载过程并替换内容:
src/component.js
export default (text = "Hello world") => {
const element = document.createElement("div");
element.className = "pure-button";
element.innerHTML = text;
element.onclick = () =>
import("./lazy")
.then(lazy => {
element.textContent = lazy.default;
})
.catch(err => {
console.error(err);
});
return element;
};
如果打开应用程序(npm start
)并单击按钮,则应在按钮中看到新文本。
如果你运行 npm run build
,应该看到一些东西:
Hash: 063e54c36163f79e8c90
Version: webpack 4.1.1
Time: 3185ms
Built at: 3/16/2018 5:04:04 PM
Asset Size Chunks Chunk Names
0.js.map 198 bytes 0 [emitted]
0.js 156 bytes 0 [emitted]
main.js 2.2 KiB 2 [emitted] main
main.css 1.27 KiB 2 [emitted] main
vendors~main.css 2.27 KiB 1 [emitted] vendors~main
...
那个 0.js 是你的分割点。检查文件可以看到 Webpack 已将代码包装在一个webpackJsonp
块中并处理了代码位。
如果要调整块的名称,请设置
output.chunkFilename
。例如,将其设置为"chunk.[id].js"
,将为每个拆分块添加单词“chunk”。
bundle-loader 提供类似的功能,但它是通过 loader 的形式,设置其
name
选项可以为 bundle 命名。
动态加载一张讲述了更复杂的拆分技术。
React 中的代码拆分
代码拆分逻辑可以包装到 React 组件中。Airbnb 使用 Joe Lencioni 描述的以下解决方案:
import React from "react";
// Somewhere in code
<AsyncComponent loader={() => import("./SomeComponent")} />
class AsyncComponent extends React.Component {
constructor(props) {
super(props);
this.state = { Component: null };
}
componentDidMount() {
this.props.loader().then(
Component => this.setState({ Component })
);
}
render() {
const { Component } = this.state;
const { Placeholder, ...props } = this.props;
return Component ? <Component {...props} /> : <Placeholder />;
}
}
AsyncComponent.propTypes = {
loader: PropTypes.func.isRequired,
Placeholder: PropTypes.node.isRequired,
};
react-async-component 在
createAsyncComponent
中包装了代码分割逻辑,并提供了服务器端渲染功能。可加载组件是另一种选择。
禁用代码拆分
默认情况下,代码拆分是不错的选择,但它不是在每个场景下都试用,尤其是在服务器端使用时。因此,可以按下面的方式将其禁用:
const webpack = require("webpack");
...
module.exports = {
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
};
请参阅 Glenn Reyes 的详细解释。
总结
代码拆分是一项功能,可让您进一步提升应用质量。您可以在需要时加载代码,以获得更快的初始加载时间和更好的用户体验,尤其是在带宽有限的移动环境中。
回顾一下:
- 代码拆分需要额外的努力,因为您必须决定分割什么以及在哪分割。通常,您会在路由中找到良好的分裂点。或者您注意到只有在使用特定功能时才需要特定代码,图表的绘制就是一个很好的例子。
- 要使用动态
import
语法,Babel 和 ESLint 都需要仔细调整。Webpack 本身支持这一语法。 - 使用
optional name
将单独的拆分点拉入相同的包中。 - 这些技术可以在现代框架和 React 等库中使用。您可以将相关逻辑打包到特定组件中,然后在合适的时机运行这个组件,以达到一个良好的用户体验。
- 要禁止代码分割,调用
webpack.optimize.LimitChunkCountPlugin
,并将maxChunks
设置为 1。
您将学习在下一章中学会清理打包。
React 项目中实现客户端搜索附录包含了一个完整的代码拆分的例子。它展示了用户输入搜索信息时如何动态加载静态站点的内容。