【译】懒加载组件

3,082 阅读6分钟

React 16.6 的新发布带来了一些只需很小努力就能给React组件对增加了很多力量的新特性。

其中有两个是 React.SuspenseReact.lazy(), 这个很容易用在代码分割和懒加载上。

这篇文章关注在如何在 React 应用中使用两个新特性和他们给 React 开发者带来的新的潜力。

代码分割

过去几年写 JavaScript 应用的方式进化了。在 ES6(modules)的出现,Babel 编译器,和其他打包工具像是 WebPack 和Browserify,JavaScript 应用现在可以用完全现代化的模式写出容易维护的东西。

通常,每个模块被导入合并在一个文件叫做 bundle,这些 bundle 在一张页面上包括了整个APP。然而,当 APP 增长的时候,这些 bundle 尺寸开始变得越来越大,因此影响了页面加载时间。

打包工具像是 Webpack 和 Browserify 提供了代码分割的支持,可以在需要加载(懒加载)而不是一次性加载不同的 bundles 中引入分割代码,从而提高 app 的表现。

Dynamic Imports

代码风格的主要方式之一是使用动态导入。动态导入作用于 import() 语法,这还不是 JavaScript 语言标准的一部分,但是一个期望不久被接受的提案。

调用 import() 去加载模块依赖 JavaScript 的 Promises。因此,返回一个完整的加载的模块或者如果模块不存在的话就拒绝。

对于老的浏览器,es6-promise 补充应该用来补充 Promise

这儿有一个用 Webpack 打包的app的内容,看起来是动态导入模块:

import(/* webpackChunkName: "moment" */ 'moment')
.then(({default: moment}) => {
  const tommorrow =moment().startOf('day').add(1, 'day');
  return tomorrow.format('LLL');
})
.catch(error => console.error("..."))

当 Webpack 看到这样的语法,它会为 moment 库,动态创建一个分割包。

对于 React 应用,如果使用 create-react-app 或者 Next.js,代码分割在 import()中悄悄产生。

然而,如果自定义了 Webpack的设置,你需要检查 Webpack 指导。对于 Babel 转化,你需要 yarnpkg.com/en/package/… 插件,允许 Babel 正确解析 import()

React 组件的代码分割

已经有几种技术应用于 React 组件的代码分割上。常见的实现是动态 import()在应用中懒加载路由组件——这个通常是作为基于路由代码分割的组件。

然而,这里有个叫 React-loadable 的非常流行的包用于 React 组件的代码分割。它提供一个高阶函数用 promise 来加载 React 组件,实现动态 import() 语法。

考虑下面叫做 MyComponent 的 React 组件:

import OtherComponent from './OtherComponent';

export defautl function MyComponent() {
  return (
    <div>
      <h1>My Component</h1>
      <OtherComponent />
    </div>
  )
}

这里,OtherComponent 是不会请求直到MyComponent开始渲染。然而,因为我们静态导入了 OtherComponent,它会和 MyComponent 一起打包。

我们可以使用 react-loadable 去延迟加载 OtherComponent,直到我们渲染MyComponent,从而代码分割成几个包。这里有个用 react-loadable 懒加载的OtherComponent

impoort Loadable from 'react-loadable';

const LoadableOtherComponent = loadable({
  loader: () => import('./OtherComponent'),
  loading: () => <div>Loading...</div>
});

export default function MyComponent() {
  return (
    <div>
      <h1>My Component</h1>
      <LoadableOtherComponent/>
    </div>
  )
}

在这里能看到在选择对象中,组件被动态 import() 语法导入,赋值给 loader 属性。

React-loadable 也是用了 loading 属性去具体指出当等待真正组件加载时,将会渲染的回调组件。

你可以在这篇文档中了解你能通过 react-loadable 实现什么。

使用 Suspense 和 React.lazy()

在 React 16.6 中,支持基础组件的代码分割和懒加载已经通过 React.lazy()React.Suspense 添加。

React.lazy() 和 Suspense 还没有支持服务端。服务端的代码分割,仍然使用 React-Loadable。

React.lazy()

React.lazy() 很容易创建一个使用动态 import 的组件,而且像常规组件一样渲染。当组件被渲染时,它会自动打包包含这个加载的组件。

当调用 import() 加载组件,React.lazy()` 使用一个必须返回一个 promise 的参数的方法。这个默认导出包含 React 组件返回的 Promise 处理了模块。

当使用 React.lazy() 时,看起来像:

// 不使用 React.lazy()

import OtherComponent from './OtherComponent';

const MyComponent = () => {
  <div>
    <OtherComponent />
  </div>
};

// 使用 React.lazy()

const OtherComponent = React.lazy(() => import('./OtherComponent'));

const MyComponent = () => {
  <div>
    <OtherComonment />
  </div>
}

Suspense

一个使用 React.lazy() 的组件只会在它需要的时候被加载。

因此,这里需要展示一些占位符内容的格式,当懒加载组件正在被加载的时候,比如用一个加载指示器。 这就是 React.Suspense 所创建的。

React.Suspense 是一个包裹了懒加载组件的组件。你可以在不同的层级上使用一个 Suspense 组件包裹多个懒加载组件。

当所有懒加载组件加载后,这个 Suspense 组件使用 fallback 属性可以接受任何你想渲染的组件作为一个占位符。

import React, { Suspense } from 'react';

const LazyComponent = React.lazy(() => import('./OtherComponent'));

const MyComponent = () => {
  <div>
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  </div>
}

如果组件懒加载失败,在懒加载之上放置明显的错误边界来展示不错的用户体验。

我在 CodeSandbox 上已经创建了一个很简单的例子来演示使用 React.lazy()Suspense 作为懒加载组件。

这里有个微型的app代码:

import React, { Suspense } from "react";
import Loader from "./components/Loader";
import Header from "./components/Header";
import ErrorBoundary from "./components/ErrorBoundary";

const Calendar = React.lazy(() => {
  return new Promise(resolve => setTimeout(resolve, 5 * 1000)).then(
    () =>
      Math.floor(Math.random() * 10) >= 4
        ? import("./components/Calendar")
        : Promise.reject(new Error())
  );
});

export default function CalendarComponent() {
  return (
    <div>
      <ErrorBoundary>
        <Header>Calendar</Header>

        <Suspense fallback={<Loader />}>
          <Calendar />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

这里,一个很简单的 Loader 组件在懒加载 Calendar 组件中被创建用作回调内容。当懒组件 Calendar 加载失败,一个边界提示被创建来展示友好的错误。

我这里包裹了懒加载日历来模拟5秒延时。为了增加 Calendar 组件加载失败的概率,我也使用一个条件导入 Calendar 组件,或者返回一个promise的rejects。

const Calendar = React.lazy(() => {
  return new Promise(resolve => setTimeout(resolve, 5 * 1000)).then(
    () => Math.floor(Math.random() * 10 )>= 4 ?
     import("./components/Calendar"):
     Promise.reject(new Error())
  )
})

下面的截屏展示了当渲染的时候组件看起来的示例。

test

命名导出

如果你希望使用一个命名的导出组件,那么你需要再次导出他们,作为在独立的中间模块中的默认导出。

如果你有一个 OtherComponent 作为命名导出模块,你希望使用 React.lazy() 来加载 OtherComponent,那么你需要创建一个中间模块来再次导出 OtherComponent 作为 默认模块。

Component.js

export const FirstComponent = () => {/* 组件逻辑 */}

export const SecondComponent = () => {/* 组件逻辑 */}

export const OtherComponent = () => {/* 组件逻辑 */}

OtherComponent.js

export { OtherComponet as defatul } from './Components';

这时候你可以使用 React.lazy() 去加载 OtherComponent 从中间模块。

懒加载路由

使用 React.lazy()Suspense,现在很容易处理基于路由的代码分割而不使用其他外部依赖。你可以简单地转化应用的路由组建成为懒加载组件,包裹所有的路由通过 Suspense 组件。

下面的代码使用 React Router 展示了基于路由的代码分割:

import React, { Suspense } from 'react';
import { Router } from '@reach/router';
import Loading from './Loading';

const Home = React.lazy(() => import('./Home'));
const Dashboard = React.lazy(() => import('./Dashboard'));
const Overview = React.lazy(() => import('./Overview'));
const History = React.lazy(() => import('./History'));
const NotFound = React.lazy(() => import('./NotFound'));

function App() {
  return (
    <div>
      <Suspense fallback={<Loading />}>
        <Router>
          <Home path="/" />
          <Dashboard path="dashboard">
            <Overview path="/" />
            <History path="/history" />
          </Dashboard>
          <NotFound default />
        </Router>
      </Suspense>
    </div>
  )
}

总结

With the new React.lazy()React.Suspense, code-splitting and lazy-loading React components has been made very easy.

现在开始从 React 16.6享受吧。

pic