如何使用 React lazy 拆分代码

React.lazy 为什么?

lazy 函数允许我们动态导入组件。我们可能希望这样做以减少用户必须下载才能在屏幕上看到内容的初始捆绑包大小。

假设我们的应用程序分为几条路线。你有 /home 路由和 /large-page 路由。 /large-page 路由导入并使用一些我们不在 /home 页面上使用的大型库。如果用户访问我们的 /home 页面,我们不希望他们必须下载大型库才能在屏幕上呈现内容 – 他们甚至可能不会访问您的 /large-page 路由,因此浪费。

你会发生的是加载足够的 javascript 来渲染 /home 路由以进行快速初始渲染,然后如果用户导航到 /large-page 路由,您将显示一个加载微调器以通知用户一些转换即将进行发生并加载 /large-page 路由所需的 javascript 块。

互联网上的大多数人习惯于在页面之间导航时必须等待转换。更糟糕的用户体验是我们的用户长时间看一个白色的空白屏幕。

那么让我们看看 React.lazy 如何帮助我们处理这个问题。

示例

让我们创建一个 React 应用程序:

$ npx create-react-app react-lazy --template typescript
$ cd react-lazy
$ npm install react-router-dom
$ npm install moment
$ npm install --save-dev @types/react-router-dom
$ npm start

你只需要 index.tsx 和 App.tsx 文件,你可以删除 .css 和 .test 文件。

我们来看看 src/index.tsx 的内容

src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import {App} from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root'),
);

以及 src/App.tsx 的内容:

src/App.tsx

import {BrowserRouter as Router, Link, Route, Switch} from 'react-router-dom';
import Home from './Home';
import LargePage from './LargePage';

export function App() {
  return (
    <Router>
      <div>
        <Link to="/">Home</Link>
        <hr />
        <Link to="/large-page">Large Page</Link>
        <hr />
      </div>
      <Switch>
        <Route exact path="/">
          <Home />
        </Route>
        <Route exact path="/large-page">
          <LargePage />
        </Route>
      </Switch>
    </Router>
  );
}

首先,我们从 react-router-dom 导入一些组件,然后导入一些我们还没有编写的本地组件,然后我们有一个简单的导航,其中包含 2 个链接和我们在 / 和 /large-page 路由上的组件。 现在让我们添加组件。 首先让我们创建 src/Home.tsx 组件:

src/Home.tsx

export default function Home() {
  return <h1>This is the home page...</h1>;
}

和 src/LargePage.tsx 组件:

src/LargePage.tsx

import * as moment from 'moment';

export default function LargePage() {
  const a = moment.duration(-1, 'week').humanize(true, {d: 7, w: 4}); // a week ago

  return (
    <div>
      <h1>{a}</h1>
    </div>
  );
}

在我们的 LargePage 组件中,我们导入 moment 库并使用它,但是我们的 Home 路由中不需要 moment 库。 现在我们可以在路由之间导航,但即使用户从未去过 LargePage 路由,他们仍然必须在初始渲染时下载时刻库。

现在让我们改变这种行为,我们只希望用户在导航到该路由时下载矩库和 LargePage 路由的组件代码。 让我们将 src/App.tsx 编辑为:

src/App.tsx

-  import Home from './Home';
-  import LargePage from './LargePage';
+ import {lazy} from 'react';

+ const Home = lazy(() => import('./Home'));
+ const LargePage = lazy(() => import('./LargePage'));

添加悬念边界

如果现在查看浏览器,应该会看到一个大错误。

Error: A react component suspended while rendering, but no fallback UI was specified. Add a Suspense fallback= component higher in the tree to provide a loading indicator or placeholder to display.。

所以 react 告诉我们一个组件“suspended”,但是我们没有提供一个加载组件来在该组件挂起时渲染。 Suspended 表示组件尚未准备好渲染,因为尚未满足它的要求。转到 Home 路由,打开 devtools,选择网络选项卡并过滤 JS 文件,我们可以看到 Home 路由有一个单独的 JS 块,因为我们正在懒惰地导入 Home 组件。 React 尝试渲染它,但它尚未加载,因此组件暂停并且必须显示加载状态后备组件,但我们没有提供。

顺便说一句,主页组件太小了,我们不应该懒惰地加载它,对大小为 1Kb 的模块的额外网络请求是一种浪费,我们只是为了示例而这样做的。

Suspense 让我们的组件在渲染之前等待某些东西,在等待时显示回退。让我们看看它是如何工作的,再次编辑 src/App.tsx 页面并将其更改为:

src/App.tsx

import {lazy, Suspense} from 'react';
import {BrowserRouter as Router, Link, Route, Switch} from 'react-router-dom';

const Home = lazy(() => import('./Home'));
const LargePage = lazy(() => import('./LargePage'));

export function App() {
  return (
    <Router>
      <div>
        <Link to="/">Home</Link>
        <hr />
        <Link to="/large-page">Large Page</Link>
        <hr />
      </div>
      {/* Now wrapping our components in Suspense passing in a fallback */}
      <Suspense fallback={<h1>Loading...</h1>}>
        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route exact path="/large-page">
            <LargePage />
          </Route>
        </Switch>
      </Suspense>
    </Router>
  );
}

我们所要做的就是将我们的组件包装在一个 Suspense 边界中,并为加载状态传递一个后备。

如果我们在 devtools 中打开网络选项卡,将网络速度设置为慢 3G 并刷新页面,您应该能够看到我们的后备被呈现到页面,同时正在加载 Home 路由的 JS 块。

或者,如果我们在浏览器中安装了 react devtools 扩展,您可以手动挂起该组件。点击 react 扩展中的 Components 选项卡,选择 Home 组件,然后点击右上角的秒表图标,将选中的组件挂起。

到目前为止一切顺利,现在让我们再次打开网络选项卡,按 JS 文件过滤并导航到大页面路由。你会看到我们加载了 2 个 JS 块,一个用于组件本身,另一个用于时刻库。

在这种状态下,当我们导航到大页面路由时,我们的应用程序会延迟加载时刻库。如果用户访问我们的主页并且从未访问过大页面,他们甚至不必加载即时库或大页面组件的代码。

作为旁注,用户只需加载一次 JS 块。如果它们来回导航,浏览器已经缓存了文件,我们将看不到后备加载微调器,组件不必暂停。


添加错误边界

我们的应用程序似乎处于良好状态,但是我们正在使用网络来请求我们拆分的 JS 文件,所以如果用户加载我们的主页,失去与互联网的连接并导航到 LargePage 路由会发生什么。

要对此进行测试,请转到主页,刷新,打开我们的网络选项卡并将网络状态设置为离线。现在导航到 /large-page 路径,我们应该会看到一个空白屏幕,这绝不是一件好事。

在我们的控制台中,我们收到以下错误:

Uncaught ChunkLoadError: Loading chunk 2 failed.

所以我们尝试加载 JS 块,但失败了,整个应用程序崩溃了。为了向用户提供一些反馈并记录错误以便我们修复它,我们必须包装我们的组件,这些组件可能会抛出一个 ErrorBoundary 组件。

ErrorBoundary 就像一个 try{} catch(){} 用于在它下面的组件的渲染方法中抛出的错误。

考虑它的一个好方法是:Suspense 边界在其子级尚未准备好渲染时显示一个 FallbackComponent – 它处理加载状态,而 ErrorBoundary 处理在组件的渲染方法中引发的错误。

让我们在 src/ErrorBoundary.tsx 中添加一个 ErrorBoundary 组件:

src/ErrorBoundary.tsx

import React from 'react';

export class ErrorBoundary extends React.Component<
  {children?: React.ReactNode},
  {error: unknown; hasError: boolean}
> {
  state = {hasError: false, error: undefined};

  componentDidCatch(error: any, errorInfo: any) {
    this.setState({hasError: true, error});
  }

  render() {
    if (this.state.hasError) {
      return <h1>An error has occurred. {JSON.stringify(this.state.error)}</h1>;
    }

    return this.props.children;
  }
}

让我们在 src/App.tsx 组件中使用它:

src/App.tsx

import {ErrorBoundary} from './ErrorBoundary';
// ...

export function App() {
  return (
    <Router>
      <div>
        <Link to="/">Home</Link>
        <hr />
        <Link to="/large-page">Large Page</Link>
        <hr />
      </div>
      {/* Now wrapping our components in Suspense passing in a fallback */}
      <ErrorBoundary>
        <Suspense fallback={<h1>Loading...</h1>}>
          <Switch>
            <Route exact path="/">
              <Home />
            </Route>
            <Route exact path="/large-page">
              <LargePage />
            </Route>
          </Switch>
        </Suspense>
      </ErrorBoundary>
    </Router>
  );
}

现在我们已经包装了可能会抛出 ErrorBoundary 的组件。 让我们重复测试:刷新主页,打开网络选项卡,将网络设置设置为离线,然后导航到 /large-page 路由。 我们将看到一个错误被打印到屏幕上,这比看到空白屏幕更好。


限制

  • ErrorBoundary 组件是一个类,在撰写本文时,我们只能使用类来实现错误边界。
  • 我们延迟加载的组件是默认导出 – React.lazy 当前仅支持默认导出。

总结

React.lazy 允许我们将代码拆分成块。 为了提高大型应用程序的性能,我们不想强迫用户下载包含我们整个应用程序的单个 JS 文件,因为他们可能不会使用我们的整个应用程序,他们不会访问我们网站的每条路由。

由于互联网上的大多数人习惯于在页面转换之间等待,因此提供一个加载指示器并在用户导航到它时按需加载组件代码比让所有用户加载他们可能不需要的代码更好。