如何使用 React lazy 拆分代码

React.lazy – 为什么

How to split your code with React lazy

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

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

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

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

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

示例

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

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.tsxApp.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不需要该moment库。Home现在我们可以在路线之间导航,但即使用户从未去过LargePage路线,他们仍然必须moment在初始渲染时下载库。

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'));

添加悬念边界

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

错误:渲染时挂起的 React 组件,但未指定回退 UI。在树的更高层添加一个 Suspense fallback= 组件,以提供要显示的加载指示器或占位符。

所以 React 告诉我们组件“已暂停”,但我们没有提供加载组件来在该组件暂停时进行渲染。暂停意味着组件尚未准备好呈现,因为它尚未满足要求。转到Home路由,打开 devtools,选择
network选项卡并过滤JS文件,你可以看到我们有一个单独的 JS
用于Home路由,因为我们正在延迟导入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边界中,并传入加载状态的回退。

如果您在开发者工具中打开您的网络选项卡,将网络速度设置为Slow 3G并刷新页面,您应该能够看到我们的回退被渲染到页面,同时正在加载 Home 路由的JS 块。

或者,如果您的浏览器中安装了 React devtools 扩展,您可以手动挂起该组件。点击Componentsreact extension中的tab,选中Home组件,点击右上角的秒表图标,即可挂起选中的组件。

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

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

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

添加错误边界

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

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

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

未捕获的 ChunkLoadError:加载块 2 失败。

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

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

考虑它的一个好方法是:当它的孩子还没有准备好渲染时,Suspense边界显示 a
– 它处理加载状态,而
处理组件渲染方法中抛出的错误。
FallbackComponentErrorBoundary

让我们在以下位置添加一个ErrorBoundary组件src/ErrorBoundary.tsx

// 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组件中使用它:

// ... other imports 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. 让我们重复测试:刷新Home页面,打开网络选项卡,将网络设置设置为离线并导航至
/large-page路线。您将看到错误被打印到屏幕上,这比看到空白屏幕更好。

限制

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

摘要

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

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

发表评论