如何使用 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 文件,因为他们可能不会使用我们的整个应用程序,他们不会访问我们网站的每条路由。
由于互联网上的大多数人习惯于在页面转换之间等待,因此提供一个加载指示器并在用户导航到它时按需加载组件代码比让所有用户加载他们可能不需要的代码更好。
相关文章
Node.js 与 React JS 的比较
发布时间:2023/03/27 浏览次数:137 分类:Node.js
-
本文比较和对比了两种编程语言,Node.js 和 React。React 和 Node.js 都是开源 JavaScript 库的示例。 这些库用于构建用户界面和服务器端应用程序。
在 Kotlin 中使用 Lazy 和 Lateinit 初始化属性
发布时间:2023/03/22 浏览次数:108 分类:编程语言
-
Kotlin 允许使用 lazy 和 lateinit 关键字进行属性初始化。本文阐明了它们的含义以及如何在 Kotlin 中使用它们。
在 TypeScript 中 React UseState 钩子类型
发布时间:2023/03/19 浏览次数:200 分类:TypeScript
-
本教程演示了如何在 TypeScript 中使用 React useState hook。
TypeScript 中的 React 事件类型
发布时间:2023/03/19 浏览次数:162 分类:TypeScript
-
本教程演示了如何在 TypeScript 中为 React 事件添加类型支持。
在 React 中循环遍历对象数组
发布时间:2023/03/18 浏览次数:124 分类:React
-
在 React 中循环对象数组: 使用 map() 方法迭代数组。 我们传递给 map() 的函数会为数组中的每个元素调用。 该方法返回一个新数组,其中包含传入函数的结果。 export default function App (
获取 React 中元素的类名
发布时间:2023/03/18 浏览次数:162 分类:React
-
在 React 中使用 event.target 获取元素的类名 获取元素的类名: 将元素上的 onClick 属性设置为事件处理函数。 访问元素的类名作为 event.currentTarget.className 。 export default function App () { cons
如何将 key 属性添加到 React 片段
发布时间:2023/03/18 浏览次数:152 分类:React
-
使用更详细的片段语法将 key 属性添加到 React 片段,例如 React.Fragment key={key} 。 更冗长的语法实现了相同的结果对元素列表进行分组,而不向 DOM 添加额外的节点。 import React from react
如何在 React 中删除事件监听器
发布时间:2023/03/15 浏览次数:203 分类:React
-
在 React 中删除事件监听器: 在 useEffect 挂钩中添加事件侦听器。 从 useEffect 挂钩返回一个函数。 当组件卸载时,使用 removeEventListener 方法移除事件监听器。 import {useRef, useEffect} from r