React Suspense在三种不同的架构中使用

React Suspense的发展历程有些奇特:仅仅是一种渲染加载状态的花哨方式,多年来它鲜少被使用。然而,随着最近发布的React 18Suspense提供了一整套新的好处,值得重新关注。不幸的是,这些优势很大程度上取决于您应用的架构。让我们看看当今最常见的三种渲染架构以及React Suspense可以扮演的角色。

  • 客户端渲染:在React.lazy加载时显示备用内容;在使用与Suspense兼容的框架获取数据时,声明式地处理加载/错误状态。
  • 服务器端渲染:以上所有内容+在<Suspense />中包装的服务器端渲染组件在客户端被有选择地hydrated
  • 服务器组件:以上所有内容+在<Suspense />中包装的异步服务器组件以阶段方式流式传输到客户端:首先是备用内容,然后是最终内容。
    现在深入探讨一下!

客户端渲染

这是React的基本用法。在请求时,服务器响应一个简单的html文件,其中包含一个<script>标签引用一个javascript包。当javascript加载并执行时,它会生成页面上的内容,并填充我们空的html文件。导航完全是客户端的,不会向服务器发出额外的请求 - 这导致我们对Suspense的第一个用例。考虑到我们的javascript捆绑包包含生成应用程序任何部分所需的代码,它可能会变得相当庞大。由于必须加载、解析和执行整个javascript文件,然后才能渲染页面的内容,这成为了一个严重的性能瓶颈。但当然,您实际上不需要在每个页面上的每个部分都生成应用程序的代码。如果我们能够将应用程序拆分成几个不同的javascript捆绑包,并且只在需要时将每个发送到客户端,那会怎样呢?这就是SuspenseReact.lazy的作用。

使用React.lazy的Suspense

在其核心,React.lazy允许您通过传递一个返回到组件的Promise的函数来延迟加载React组件。然而,在大多数情况下,您会看到它与动态导入语法一起使用,以延迟加载另一个模块。

1
const Post = lazy(() => import('./Post.ts'));

Suspense配对,您可以指示React在导入加载时呈现一个回退加载状态:

1
2
3
4
5
6
7
export default function Wrapper() {
return (
<Suspense fallback={<div>Loading ...</div>}>
<Post />
</Suspense>
)
}

如果您使用像React Router这样的导航库,您可以通过路由进行应用程序的代码拆分,在您的Route组件中分别延迟加载每个页面的入口点。

您可以自己实现此行为 - 在动态导入组件时呈现加载状态 - 而不使用SuspenseReact.lazy,但是使用Suspense更加优雅。然而,这引发了一个问题:如果我们能够简化在useEffects中进行的所有数据获取操作呢?

在 useEffect 中使用 Suspense 进行数据获取

在我们进一步探讨之前,是时候快速了解一下<Suspense />的内部情况了。主要是,父级 <Suspense> 如何知道其子组件何时正在加载?据我所知,子组件改变其父组件状态的方式只有两种:

  • 子组件修改了父组件中使用的状态
  • 子组件抛出一个值,父组件可以捕获并处理

这第二个选项通常以错误边界的形式出现,错误边界是一个React组件,设计用于捕获应用程序发生错误时子组件意外抛出的错误。有趣的是,React已经利用了这个机制不仅仅用于抛出错误:Suspense依赖于子组件抛出一个Promise

简而言之,子组件抛出一个处于挂起状态的Promise,当它准备好渲染时解析。父组件捕获此Promise,并根据需要渲染回退属性或子组件的内容。

可以想象,设置自己的支持Suspense的数据获取工具可能会相当复杂,您可能最好让一个库来处理。

这就是说,如果您的库支持它,您可以将组件包装在 <Suspense /> 中,指定一个回退,添加一个错误边界来捕获任何被拒绝的Promise,并且您再也不用担心isLoadingisError状态了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Post() {
// 例如使用React Query
const { data } = useQuery({ suspense: true })

// 您可以假设数据将被完全获取,因为React会在数据加载时“挂起”组件,
// 并且此代码不会被执行!
return <div>{data}</div>
}

export default function Wrapper() {
return (
<ErrorBoundary fallbackRender={<div>Error!</div>}>
<Suspense fallback={<div>Loading ...</div>}>
<Post />
</Suspense>
</ErrorBoundary>
)
}

对于某些人来说,这可能看起来更简单,但其他人可能会认为这是“金字塔的初级阶段”,并更喜欢以命令式方式处理加载和错误状态。无论哪种方式,很难说数据获取与 Suspense 开启了您无法自己实现的任何功能。那些功能以后会到来。

服务器端渲染

在服务器端渲染的应用程序中,Suspense开始解锁一些非常有趣的新功能 - 但首先,让我们简单了解一下水合的基础知识。

在请求时,您的元框架将通过运行从相关文件导出的组件来生成给定页面的html,以生成首次渲染的html。这个html将被发送给用户,以便在javascript包加载和执行时,他们可以查看一些有意义的内容。一旦javascript到达,框架将在客户端重新运行您的组件,并确保生成的dom与服务器上生成的html相同(如果不同,您可能会收到警告)。此时,我们拥有了在服务器上生成的相同dom,但是所有与创建状态、绑定事件等相关的javascript也都已经就位了。在客户端重新运行组件的整个过程被称为水合。

与客户端渲染相比,服务器端渲染在第一次页面加载时提供了更好的用户体验,因为用户可以在javascript包加载和执行时查看一些由服务器生成的html。然而,用户只能看看 - 没有javascript,页面无法与之交互。放大这个问题的是,整个页面都需要在任何部分可以与之交互之前完成hydrated!这就是 Suspense 的第三个用例:选择性hydrated

通过将组件包装在 Suspense 中,React将单独对其进行水合处理,而不是与页面的其余部分一起。乍一看,这可能不太有利:如果所有脱水的html一起发送到客户端,那么整个页面将同时进行hydrated处理 - 无论是否进行选择性hydrated。不过,这并不完全准确,有两个原因:

  • 如果我们将几个组件包装在Suspense中,React可以根据用户在某个特定时间与之交互的组件来智能地决定首先进行哪个组件的水合处理。换句话说,React可以优先处理页面的哪个部分,并在后台逐步为用户提供可交互的小部件,同时后续进行页面的水合处理。在速度较慢的设备上 - 解析和执行javascript可能是真正的瓶颈 - 这可以为用户创造出更快的体验。

  • 使用流式架构,页面的不同部分可以分别发送到客户端,这意味着可以将给定的html块发送到客户端并进行选择性hydrated处理,同时页面的其他部分仍在服务器上工作以进行渲染!关于流式处理的更多信息:

请注意,当前一代SSR框架不支持选择性hydrated - 就我所知,只有使用 app 目录的Next.js应用程序支持选择性hydrated,用于在服务器上呈现为html的客户端组件。

服务器组件

简而言之,服务器组件是在发送到客户端之前在服务器上呈现为html的React组件。这听起来可能像是服务器端渲染,但服务器组件仅在服务器上运行;它们永远不会在客户端上运行。它们不能使用事件处理程序、状态或钩子,并且从根本上来说是非交互式的。相反,服务器组件被优化用于获取和渲染静态数据:

1
2
3
4
5
export default async function Post() {
const data = await fetch(...)

return <div>{data}</div>
}

请注意,该函数/组件是异步的!您只需等待数据加载,然后将您的内容渲染为html并发送到客户端。简单而优雅,但不是一个很好的用户体验 - 直到异步操作完成,您的组件被阻塞,并且用户从您的组件看不到任何内容!事实上,这种情况正是加载状态被创建的原因。那么我们如何给我们的服务器组件添加一个加载状态呢?使用Suspense

通过将您的异步服务器组件包装在 <Suspense /> 中,React将在组件获取数据时渲染并发送回退内容到客户端。一旦数据加载完成,它将从组件本身发送渲染的内容。这种随时间将多个html块发送到客户端的过程称为流式传输。

1
2
3
4
5
6
7
8
9
10
11
12
13
async function Post() {
const data = await fetch(...)

return <div>{data}</div>
}

export default function Wrapper() {
return (
<Suspense fallback={<div>Loading ...</div>}>
<Post />
</Suspense>
)
}