代码分割
随着功能的演变,Web应用程序往往会越变越大,应用程序加载所需的时间也越来越长。但对用户来说体验效果就越令人沮丧。在网络速度较慢的移动环境中,此问题会被无限放大。
即使分离构建包可以在一定的程度上缓解这个问题,但它不是唯一的解决方案,你最终仍然需要下载大量数据。幸运的是,使用代码分割这个技术可以更好的解决这个问题。它允许在你需要时动态地加载代码。
你可以在用户进入应用程序的新视图时加载更多代码。你还可以将加载绑定到特定的用户操作上,例如滚动或单击按钮。你还可以尝试预测用户下一步要做什么,并根据你的猜测加载代码。这样当用户尝试访问它时,功能就已存在。
顺便说一下,使用webpack的懒加载可以实现Google的[PRPL模式](https://developers.google.com/web/fundamentals/performance/prpl-pattern/)。PRPL(推送,渲染,预缓存,延迟加载)的设计考虑了移动网络。
代码分割的格式
代码分割可以在webpack中以两种主要方式完成:通过动态的 import
或 require.ensure
语法。在此项目用前者。
项目的目标是最终得到一个按需加载的分裂点。可以在拆分中进行拆分,你可以根据拆分构建整个应用程序。这样做的好处是,你的应用程序的初始有效负载可能会比其他情况下更小。
import
的动态特性
[dynamicimport
语法](https://github.com/tc39/proposal-dynamic-import)尚未在官方语言规范中。 由于这个原因,特别是在Babel设置中需要进行小的调整。
动态的import是用 Promise
定义的:
import(/* webpackChunkName: "optional-name" */ "./module").then(
module => {...}
).catch(
error => {...}
);
可选名称允许你将多个拆分点拉入单个包中。只要它们具有相同的名称,它们就会被分组。每个拆分点默认生成一个单独的包。
接口允许组合,你可以并行加载多个资源:
Promise.all([
import("lunr"),
import("../search_index.json"),
]).then(([lunr, search]) => {
return {
index: lunr.Index.load(search.index),
lines: search.lines,
};
});
上面的代码为请求创建分离的包。如果只需要一个,则必须使用命名或定义中间模块来 import
。
在以正确的方式配置之后,语法仅适用于JavaScript。如果你使用其他环境,则可能必须使用以下各部分中介绍的替代方案。
有一个旧的语法require.ensure。实际上,新语法可以涵盖相同的功能,另见[require.include](https://webpack.js.org/api/module-methods/#require-include)。
[webpack-pwa](https://github.com/webpack/webpack-pwa)大范围地介绍了这个思想,并讨论了不同的基于shell的方法。你可以在 Multiple Pages 这章中将详细介绍这个主题。
配置代码分割
为了演示代码分割的想法,你可以使用动态的 import
。项目的Babe需要额外的l配置以使语法有效。
配置 Babel
鉴于Babel不支持开箱即用的动态 import
语法,它需要[@babel/plugin-syntax-dynamic-import](https://www.npmjs.com/package/@babel/plugin-syntax-dynamic-import)插件来工作。
用下面的命令,添加 @babel/plugin-syntax-dynamic-import
依赖:
npm install @babel/plugin-syntax-dynamic-import --save-dev
在 .babelrc 中添加如下配置启用动态的 import
:
{
"plugins": ["@babel/plugin-syntax-dynamic-import"],
...
}
如果你正在使用ESLint,你应该在ESLint配置中安装
babel-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](https://www.npmjs.com/package/bundle-loader)可以通过loader接口实现类似的结果,它通过
name
选项支持bundle重命名。
当你必须处理更复杂的分割时,Dynamic Loading 这章介绍了其他技术。
使用React实现代码分割
在React组件中实现拆分模式。Airbnb使用以下[解决方案](https://gist.github.com/lencioni/643a78712337d255f5c031bfc81ca4cf),如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
调用,并提供服SSR特定功能。 loadable-components 是其它一个选项。
禁用代码分割
虽然在一般况下代码分割是好的,但它并不是总是好的,尤其是在服务器端使用时。因此,可以像下面这样将其禁用:
const webpack = require("webpack");
...
module.exports = {
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
};
为什么代码分割在服务端使用会出现问题?,可以看 Glenn Reyes 的详细解释。
总结
代码分割是一个可以进一步提升你的应用程序的功能。你可以在需要时加载代码,以获得更快的初始加载时间和改善的用户体验,尤其是在带宽有限的移动环境中。
内容回顾:
- 要实现 代码分割 ,你必须决定分割什么和在哪里。通常,你会在路由器中找到良好的分裂点。或者你注意到仅在使用特定功能时,它才被需要。
- 要使用动态
import
语法,Babel和ESLint都需要仔细调整。 Webpack支持开箱即用的语法。 - 使用命名将分离的拆分点放入相同的构建包中。
- 这些技术可以在框架和React等库中使用。你可以将相关逻辑包装到特定组件,用户友好的方式处理加载过程。
- 启用代码分割,可以用
webpack.optimize.LimitChunkCountPlugin
的maxChunks
配置。
在下一章中,将介绍如何对构建做整理。
附录中的 Searching with React 包含代码分割的完整示例。当用户搜索信息加载时,如何设置静态站点索引的显示。