【译】懒加载组件
React 16.6 的新发布带来了一些只需很小努力就能给React组件对增加了很多力量的新特性。
其中有两个是 React.Suspense
和 React.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 转化,你需要 https://yarnpkg.com/en/package/babel-plugin-syntax-dynamic-import 插件,允许 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())
)
})
下面的截屏展示了当渲染的时候组件看起来的示例。
命名导出
如果你希望使用一个命名的导出组件,那么你需要再次导出他们,作为在独立的中间模块中的默认导出。
如果你有一个 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享受吧。