代码拆分

随着功能的增加,Web 应用也越来越大。应用程序加载所需的时间越长,对用户来说就越令人沮丧。在连接速度较慢的移动环境中,该问题还会被放大。

即使拆分打包可以帮助您提升一个档次,但它们并不能完全解决问题,您仍然需要下载大量数据。幸运的是,由于代码拆分,可以更好地解决这个问题。它可以根据您的需要延迟加载代码。

您可以在用户进入应用程序的新视图时加载需要的代码。您还可以将加载绑定到特定操作,例如滚动或单击按钮。您还可以尝试预测用户下一步要做什么,并根据您的猜测加载代码。这样,当用户尝试访问它时,功能就已存在。

顺便说一下,使用 Webpack 的延迟加载可以实现 Google 的 PRPL模式。PRPL(推送,渲染,预缓存,延迟加载)的设计考虑了移动网络。

代码拆分格式

我们可以在 Webpack 中以两种主要方式完成代码拆分:通过动态 importrequire.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)并单击按钮,则应在按钮中看到新文本。

Hello-from-lazy,延迟加载代码

如果你运行 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-componentcreateAsyncComponent 中包装了代码分割逻辑,并提供了服务器端渲染功能。可加载组件是另一种选择。

禁用代码拆分

默认情况下,代码拆分是不错的选择,但它不是在每个场景下都试用,尤其是在服务器端使用时。因此,可以按下面的方式将其禁用:

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 项目中实现客户端搜索附录包含了一个完整的代码拆分的例子。它展示了用户输入搜索信息时如何动态加载静态站点的内容。

用户头像
登录后发表评论