前言

我们在编写React应用的时候,常常需要在组件里面异步获取数据。因为异步请求是需要一定时间才能结束的,通常我们为了更好的用户体验会在请求还没有结束前给用户展示一个loading的状态,然后如果发生了错误还要在页面上面展示错误的原因,只有当请求结束并且没有错误的情况下,我们才渲染出最终的数据。这个需求十分常见,如果你的代码封装得比较好的话,你的处理逻辑大概是这样的:

代码语言:javascript代码运行次数:0运行复制const AsyncComponent = () => {

const [data, isLoading, error] = fetchData('./someapi')

if (isLoading) {

return

}

if (error) {

return

}

return

data={data}

/>

}

在上面的代码中我展示了大多数项目里面常用的做法,那就是:封装一个自定义的hook(fetchData) 来处理异步请求的不同状态 - pending, error和success。这种做法一般情况下是没有什么问题的,至少比没有封装要好很多,可是当我们的项目规模变大了以后,你会发现我们还是需要写很多模板代码,因为每次调用完fetchData都需要判断isLoading和error的值然后展示相对应的内容。那么有没有一种办法可以让我们在某些地方统一处理pending和error的情况,从而我们在组件里面只需要处理success的情况呢?答案是肯定的,本篇文章将会提供一种基于Suspense和ErrorBoundary的思路来解决这个问题。

API介绍在介绍具体方案之前,我们先来看看会用到的两个组件 - Suspense和ErrorBoundary的具体用法。

SuspenseReact 16.6引入了Suspense组件,这个组件会在其子组件还处于pending状态时展示一个fallback的效果,例如:

代码语言:javascript代码运行次数:0运行复制import { Suspense } from 'react'

}>

在上面的代码中当SomeComponent处于pending状态时,Suspense会展示Loading组件。看到这里你可能会问Suspense组件是怎么知道SomeComponent处于pending状态的呢?它的原理简单来说就是这个组件会捕获子组件抛出来的异常,如果这个异常是一个promise,而且这个promise是pending状态的它就显示fallback的内容否则就渲染其子组件。

其实如果你有做过Code Spliting的优化,你大概率已经用过这个组件了,一般它会用来懒加载某个组件,例如下面的代码:

代码语言:javascript代码运行次数:0运行复制import { lazy, Suspense } from 'react'

const LazyComponent = lazy(() => import('./component'))

}>

Error BoundariesError Boundaries也是React 16引入进来的概念。它的引入是为了解决某个组件发生错误的时候整个页面crash的情况(白屏)。有了Error Boundaries这个功能后,你可以实现一个ErrorBoundary组件,这个组件可以捕获到从子组件抛出来的错误,然后你就可以对这个错误进行自定义的处理从而防止这个错误直接传递到应用的最外层导致整个应用的奔溃。以下是一个常见的ErrorBoundary组件的实现:

代码语言:javascript代码运行次数:0运行复制class ErrorBoundary extends React.Component {

constructor(props) {

super(props)

// 使用state来保存当前组件的错误信息

this.state = {error: null}

}

// 就是这个函数实现了error boundary的功能,用来返回错误出现后的state

static getDerivedStateFromError(error) {

return { error }

}

render() {

// 如果组件发生了错误那么就展示错误的信息否则渲染子组件的内容

if (this.state.error) {

return

error occur

}

return this.props.children

}

}

完整方案在介绍完我们需要用到的两个组件Suspense和ErrorBoundary后,我们终于可以来看一下实际的方案了。我们的方案很简单,总的来说就是:在需要处理异步请求的组件外面包裹一层Suspense组件和ErrorBoundary组件,其中Suspense组件处理异步请求的pending状态,而ErrorBoundary处理请求的error状态。Talk is cheap, show me the code。我们来看一下具体的代码实现:

处理异步请求的子组件假如我们需要实现一个组件,这个组件会调用一个返回随机单词的接口,当结果返回后我们需要显示返回的单词。我们这里要调用的接口是一个公共的接口,地址是https://api.api-ninjas.com/v1/randomword,调用这个接口的一个示例返回值是:

代码语言:javascript代码运行次数:0运行复制{

"word": "Stokesia"

}

接着我们来实现子组件的相关代码:

代码语言:javascript代码运行次数:0运行复制// utils/fetchData.js

// 这个函数式是对fetch函数的封装,它在请求pending和error的状态下都会抛出异常

export const fetchData = (url) => {

// 记录当前请求的状态

let status = 'pending'

// 记录请求的结果

let response

const promise = fetch(url)

.then(res => res.json())

.then(res => {

// 请求成功,转变状态

status = 'success'

// 保存请求的结果

response = res

})

.catch(error => {

// 请求失败,转变状态

status = 'error'

// 保存接口的错误信息

response = error

})

return () => {

switch(status) {

// 如果请求还在进行中就抛出promise的异常,这个promise会被外层的Suspense处理

case 'pending':

throw promise

// 如果请求出现错误就抛出错误信息,这个错误信息会被外层的ErrorBoundary处理

case 'error':

throw response

// 如果请求已经完成,就直接返回数据

case 'success':

return response

default:

throw new Error('unexpected status')

}

}

}

// RandomWord.jsx

import { fetchData } from './utils/fetchData'

// 调用上面的fetchData函数来获取一个包装完毕的fetch函数

const randomWordFetch = fetchData('https://api.api-ninjas.com/v1/randomword')

const RandomWord = () => {

const response = randomWordFetch()

// 如果代码能执行到这里就表示接口已经调用成功并且返回了

const word = response.word

return

{word}

}

export default RandomWord

外层组件编写完子组件的代码后,我们再来看看外层组件(App)的代码:

代码语言:javascript代码运行次数:0运行复制// App.jsx

import ErrorBoundary from "./ErrorBoundary"

import RandomWord from "./RandomWord"

import {Suspense} from 'react'

function App() {

return (

loading...