likes
comments
collection
share

如何使用 Router 为你页面带来更快的加载速度

作者站长头像
站长
· 阅读数 5

引言

React-Router 在 6.4 版本中 Release 了一系列 loaderFunction、defer 等 Data Apis,将数据获取和页面渲染进行分离从而带来更好的用户体验。

今天这篇文章就来和大家一起来探索 Data Apis 是如何为我们的页面带来更好的用户体验,

Why is the data apis better?

通常在以往的页面渲染中,无论是服务端渲染还是客户端渲染都无法逃过数据与页面交互造成用户体验迟钝的关系。

往往大部分页面中真正具有意义的页面元素都需要等待数据加载完成后重新渲染才可以直接展示给用户,所以优化发起数据请求的时机对于用户看到页面真正有意义的内容来说是必不可少的方式。

首先,我们先从 Client Side Render 以及 Server Side Render 两方面来分析 React Router 在未使用 Data Apis 之前是页面渲染与数据获取是如何工作的。

Client Side Render

首先,在客户端渲染中由于我们的页面是由一个一个静态资源构成并不存在服务端的概念。

自然,页面的上的关键对客展示内容的渲染更像是一个瀑布:

如何使用 Router 为你页面带来更快的加载速度

像这样的组件在我们的应用程序中数不胜数,通常我们会在各个组件挂载生命周期中发起数据请求,数据请求返回后在重新渲染携带数据的子组件。

或许,子组件中如何仍然存在数据获取请求时整个页面渲染就像是一个特别大的瀑布加载过程,显而易见这会儿导致我们的应用程序比原始的体验效果差许多。

在 V6 的 React Router 中在客户端渲染中为路由提供了 LoaderData 的概念,可以将数据请求和组件渲染分离。

简单来说,在页面接受到路由访问时就可以同步开始数据请求而无需依赖任何组件渲染:

如何使用 Router 为你页面带来更快的加载速度

通过分离渲染和数据的过程,完美的解决瀑布式的体验问题。

不要小瞧这部分数据获取带来的良好体验,图中的例子只是一次数据请求,当页面中需要加载的数据拥有一定量级时这样的方式会为我们的页面大大缩短加载/渲染时间带来更好的用户体验。

当然,在传统 SPA 应用中数据请求如何和页面渲染并行触发。同样我们会使用一个 Loading 之类的骨架来为页面展示 Loading 内容。

但是,React Router 在 6.4 的 data apis 中提供了一个 defer api 以及 Await component 来解决这一问题:选择性的推迟页面部分内容的渲染,数据渲染并不会阻塞整个页面的渲染。

稍后,我们也会为大家尝试使用 Data Apis 来体验这一过程。

Server Side Render

让我们在聚焦于服务端渲染应用,同样在服务端渲染框架中诸如 NextJs、NuxtJs 等各种框架。由于我们的应用不单单是由静态资源组件,而是拥有了服务的概念。

在 SSR 模式下,天然具有将数据获取和页面渲染分离的优势。自然,我们可以在 SPA 的基础上优化数据请求的过程。我们可以在请求到达我们的服务时立即发起数据请求:

如何使用 Router 为你页面带来更快的加载速度

即使拥有多个数据请求我们也可以方便的在请求到来时并行加载数据:

如何使用 Router 为你页面带来更快的加载速度

不过这一切都没有问题了吗?显而易见,在进行数据请求的过程中用户访问我们的页面只能得到一片白。这段时间是非常糟糕的用户体验。

那么,这部分的用户体验我们当真就没有办法了吗?

在 React 18 之前的确是没有好的办法。要么就是给用户在客户端渲染时展示 Loading 将数据仍然和渲染进行挂载,显然这并不是一个两全的办法。更像是一种取舍,在用户白屏和 Loading 态之间做选择。

但是在 React 18 之后,我们可以借助 Streaming 的过程配合 React Router 的 defer api/Await compoennt 进行针对性的部分页面渲染:

如何使用 Router 为你页面带来更快的加载速度

假设我们的页面中有 A、B 两个组件需要在获取数据后才可以进行有意义的对客内容展示,当用户访问我们的页面内容时可以看作以下过程:

  • 用户访问到我们的页面,此时开始进行 A、B 组件的数据请求同时通过 Streaming 的形式进行整个页面的渲染并返回,此时用户会看到整个页面以及仅仅只有 A、B 两个组件作为骨架态加载。

  • 之后,B 组件的服务端数据加载态完成后。客户端的 B 组件会获得这部分服务端返回的数据,页面会重新渲染 B 组件为携带数据的样式并对于 B 组件增加事件交互进行水合。

  • 在之后,A 组件的服务端数据返回后,会重复 B 组件的过程,渲染携带数据的 A 组件并进行部分水合。

完美的解决了我们在原始 SSR 下要么白屏要么选择将数据获取依赖组件渲染的两难。

或许说到这里有些同学会想到 React 18 的新特性: Server Component,的确 Server Componet 也可以完美的解决上述问题。

不过,现阶段的 Server Component 对于交互稍微复杂的网站来说更像是一种小型玩具,你不仅时刻需要注意它的编写方式同时只有极少部分组件才可以当作服务端组件进行数据获取,当然仁者见仁,起码现阶段的 RSC 在我体验后仍然是觉得有些不尽人意。

Remix.run 提供了开箱即用的上述功能,你无需任何繁琐的 SSR 应用配置即可快速在你的应用程序中体验上述功能。

快速上手

说了那么多理论知识,接下来我们就来简单体验下 Data Apis 应该如何使用。

项目demo。

createBrowserRouter

在 V6 之前通常我们会直接使用 <BrowserRouter /> 组件来作为我们应用程序的根节点,我相信大多数同学 React 应用仍是这样在使用路由。

在 V6 后提供了一种新的方式来创建路由对象 createBrowserRoute Api ,只有使用了 createBrowserRoute Api 创建的路由对象才被允许使用路由的 data apis。

自然,我们首先应该使用 createBrowserRoute 来创建一个所谓的路由对象:

// 默认数据获取方法
const getDeferredData = () => {
  return new Promise((r) => {
    setTimeout(() => {
      r({ name: '19Qingfeng' });
    }, 2000);
  });
};

const getNormalData = () => {
  return new Promise((r) => {
    setTimeout(() => {
      r({ name: 'wang.haoyu' });
    }, 2000);
  });
};
// 创建数据路由对象
const router = createBrowserRouter([
  {
    path: '/',
    Component: App,
    children: [
      {
        index: true,
        Component: Normal,
        loader: async () => {
          const data = await getNormalData();
          return json({
            data
          });
        }
      },
      {
        path: 'deferred',
        Component: Deferred,
        loader: () => {
          const deferredDataPromise = getDeferredData();
          return defer({
            deferredDataPromise
          });
        }
      }
    ]
  }
]);

上边的代码中我们使用 createBrowserRouter 创建了一个携带数据的路由对象。

  • 根路径 / : 该路径默认会渲染 Normal 组件,并且将组件与数据进行了解耦,拥有一个名为 getNormalData 的数据获取方法。

  • /deferred 路径: 该路径渲染 Deferred 组件,同样拥有一个 getDeferredData 的数据获取方式。

创建路由对象时,根路径和 deferred 路径乍一看大同小异。不过还是稍稍有些不同的:

  • 跟路径下的 loaderFunction 使用了 await 关键字,不难想象 Normal 组件的渲染是需要等待 loader 中的异步操作结束才可直接渲染。

  • /deferred 路径下的 loader 并未使用 await 关键字,而是使用了 defer 包裹了 getDeferredData 方法返回的数据请求(该方法返回一个 Promise),我们并未使用 await 去等待它完成,自然 Deferred 路径的渲染也并不会被阻塞。

RouterProvider

在调用 createBrowserRouter 获得 router 对象时,我们仍然需要在我们的根组件将创建的路由对象传递给我们的应用程序,此时就需要使用到 RouterProvider Api:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider } from 'react-router-dom';
import router from './routes/router.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router}></RouterProvider>,
  </React.StrictMode>
);

这一步我们需要将创建的路由对象传入 RouterProvider ,同时将 RouterProvider 作为应用程序的根组件传递给 createRoot Api。

useLoaderData/Suspense/Await

要使用 Router Data Apis 其实我们仅仅需要在原始的应用程序中更换上述两个创建路由对象时的 Api 即可。

接下来的部分,我们已经在路由定义时将数据请求和组件拆分开来,那么在组件渲染中我们如何获取这部分数据请求返回的数据。

ReactRouter 中提供了一个 useLoaderData 的 hook 来为我们在组件中获取路由中 loader 的加载数据:

import { useLoaderData } from 'react-router';

function Normal() {
  // 直接使用 useLoaderData 获取当前组件对应 loader 返回的数据
  const { data } = useLoaderData();

  return (
    <div>
      <h3>Hello</h3>
      <p>{data.name}</p>
    </div>
  );
}

export default Normal;

这一过程看起来行云流水般的丝滑。首先在定义路由列表时将数据和渲染拆分开来,请求到来时会同步触发数据请求和页面渲染。

当我们在页面渲染途中需要路由中定义的数据时,只需要简单的通过 useLoaderData 来获取对应数据即可。

如何使用 Router 为你页面带来更快的加载速度

当我们首次访问根路径时,应用会同时触发根路径下的 loaderFunction 等待 loaderFunction 执行完毕后使用 loaderFunction 中返回的数据进行页面渲染。

不过上边的截图中明显可以看到,在访问根路径时页面会有部分的白屏之后才开始直接渲染页面。

这是因为我们在根路径下的 loader 定义成了阻塞的异步逻辑:

        loader: async () => {
          const data = await getNormalData();
          return json({
            data
          });
        }

页面渲染需要依赖 loader 中的数据,而 loader 的执行又是一种异步的阻塞逻辑,自然首次打开页面时需要等待这部分的 loader 执行完毕才可以渲染。

虽然说这一步我们已经将页面的渲染和数据获取通过 loader 的方式拆分开来,不过由于渲染需要依赖 loader 中的数据又会造成阻塞的方式,这样的用户体验自然也是比较糟糕的。

值得庆幸的是 ReactRouter 中为我们提供了两种方式来处理这个问题:

  • 首先,第一种方式是在每次页面切换 loader 加载时,支持在顶层传入一个 fallbackElement 来渲染加载时的骨架。
// main.tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider
      router={router}
      fallbackElement={<div>Loading...</div>}
    ></RouterProvider>
  </React.StrictMode>
);

如何使用 Router 为你页面带来更快的加载速度

通过为 RouterProvider 定义 fallbackElement 元素可以在整个页面切换时增加骨架展位从而给予用户及时的加载反馈。

这种方式虽然简单,但是 fallbackElement 的方式是页面级别的加载。

有时我们的页面只有部分模块内容需要依赖 loader 的数据完成才可以渲染真正有意义的内容,大多数时候页面中的其他元素都是静态(不依赖于数据加载)的模块。

粗暴的使用 fallbackElement 在 loader 执行时阻塞整个页面的渲染并不是在站点体验上的最优解。

同时 fallbackElement 只会在页面首次加载时才会生效,当你的页面拥有多个 page 进行 SPA 跳转时,需要配合 navigation.state 来判断页面是否处于跳转加载态。

  • 另外一种方式,可以更好的解决 fallbackElement 带来的全局 loading 问题,ReactRouter 中提供了 Await Component 以及 defer function 来为我们解决上述的问题。

这次,让我们访问 /deferred 路径:

如何使用 Router 为你页面带来更快的加载速度

上边的截图中可以看到,页面在加载时可以分为两个部分:

  • 没有任何数据依赖的部分,在页面加载时会直接渲染到屏幕中。

  • 依赖数据的部分首次,首先渲染为 loading deferred data 加载状,等待 loader 加载完毕后会重新渲染为真正含有意义的部分 19Qingfeng。

需要额外留意的是,大家不要将这一部分和在 useEffect 中发起数据请求混淆。deffer router 的优势正是在于对于发起数据请求的时机优化:

如何使用 Router 为你页面带来更快的加载速度

让我们再次聚焦到 <Deffer /> 组件上:

// router
{
        path: 'deferred',
        Component: Deferred,
        loader: () => {
          const deferredDataPromise = getDeferredData();
          return defer({
            deferredDataPromise
          });
        }
}

// deffer component
import { Suspense } from 'react';
import { Await, useLoaderData } from 'react-router';

function Deferred() {
  const { deferredDataPromise } = useLoaderData()

  return (
    <>
      <div>This is deferred Page!</div>
      <div>
        <h3>Hello</h3>
        <Suspense fallback={'loading deferred data'}>
          <Await resolve={deferredDataPromise}>
            {(data) => {
              return <p>{data.name}</p>;
            }}
          </Await>
        </Suspense>
      </div>
    </>
  );
}

export default Deferred;

由于我们在路由定义时,/deferred 路径对应的 loader 中并不存在任何阻塞逻辑,同时我们通过 defer 方法返回了数据请求的 promise,此时我们并没有在 loader 中等待这个 promise 状态完成。

之后,我们在组件中使用 Suspense 配合 Await 组件来实现页面部分元素的 loading 态从而对于页面进行一种渐进式加载方式:

  • Suspense Await 中的组件会等待 defer 依赖的 promise 状态完成后渲染成为对应的元素。

  • 页面中不依赖 loader 中的数据元素会立即渲染到浏览器中。

直到这一步,我们使用 defer 配合 Await 在页面渲染和数据请求中真正做到了同步进行,给予用户更好的加载体验。

React Router 是如何实现 Defer 这一过程

Loaders 调用时机

上边的章节中我们讲到 ReactRouter 数据路由的优势以及如何在我们的站点中使用数据路由来优化我们的页面。

接下来的这个章节,我们就来简单聊聊 ReactRouter 的 Data apis 的实现思路。

首先,loader 的定义、执行不难理解,只要在用户访问到当前路径时 ReactRouter 会寻找到当前路径下匹配到的所有 route 对象,自然我们只需要在渲染 route.Component 前调用执行所有的 loader 即可:

如何使用 Router 为你页面带来更快的加载速度

packages/react-router-dom/index.tsx

createBrowserRouter 内部会通过 createRouter 创建一个路由对象(该路由对象类似 without data apis router,用来控制页面路由对象)。

创建完成后会立即调用内部的 initialize 方法初始化路由 state:

如何使用 Router 为你页面带来更快的加载速度

重点就在于 initialize 的 startNavigation 的方法,在 SPA 应用下默认 state.initialized 是 false 会进入 startNavigation 方法。

所谓 startNavigation 即是 data route apis 中内部的跳转方法,每次跳转 ReactRouter 内部都会在内部实际调用该方法。

初始化时,调用 startNavigation 会传入第二个参数 state.location (当前页面路由),即会触发当前路由 Router 逻辑。

如何使用 Router 为你页面带来更快的加载速度

startNavigation 中会进行一系列操作,比如通过 router match 来寻找当前 state.location 下的 route 对象等等,重点就在于 handleLoaders 方法。

handleLoaders 方法正是执行当前匹配路径的所有 loaders 方法,当执行完所有 loaders 获取当前路由的路由数据。

可以清楚的看到在调用 handleLoaders 方法时是 await 的阻塞逻辑,自然也就和我们上述根路径的 case match 上了。

简单来说,客户端代码在执行 createBrowserRouter 方法后就会立即进行 initialize 方法从而对于当前 location 路径寻找匹配的 route 对象执行当前路由下的 loader 方法。

当然,当我们调用 usenavigate() 返回值跳转时,同样也是通过 startNavigation 重新调用这一过程。

同时,在 initialize 方法执行完毕后会返回 createBrowserRouter 内部定义的 router 对象,该方法内部控制了当前路由的对象和保存了 router 的各个实例方法(跳转等)。

如何使用 Router 为你页面带来更快的加载速度

Loader Data 是如何关联页面渲染的

上一步我们清楚了在页面加载后,会调用 startNavigation 方法执行所有 loader 获取 loaderFunction 返回的数据。

这次,让我们再次聚焦回到 startNavigation 方法中:

如何使用 Router 为你页面带来更快的加载速度

startNavigation 在结尾会获取到当前 location 的 match (当前所有匹配的路由对象)以及 loaderData (当前所有匹配的 loader 返回值)。

之后会在结尾调用,completeNavigation 方法。顾名思义,该方法为完成跳转的方法,在 completeNavigation 中首先会进行一系列 actionData、loaderData、block 之类的判断,这部分逻辑并不是我们的重点,重点在于:

如何使用 Router 为你页面带来更快的加载速度

completeNavigation 默认会调用 updateState 去更新最新的路由数据。

如何使用 Router 为你页面带来更快的加载速度

可以看到 updateState 方法会合并获得最新的 state 状态(包含当前 location 下的最新的 loaderData 以及 match 等等),同时调用 subscribers 订阅方法来调用 subscriber 方法传入最新的 routerState。

那么,更新后的数据会被哪里订阅呢?不知道大家还记不记得我们通过 createBrowserRouter 方法创建的 router 对象会被传入 <RouterProvider router={router} /> 中。

如何使用 Router 为你页面带来更快的加载速度

RouterProvider 组件中会订阅 initialize 返回的 router 对象,当调用 updateState 更新后会通知更新 RouterProvider 的 setState 改变该组件的 state 状态。

当 router state 改变时触发 stateState 方法,更新 RouterProvider 的 state 值,同时该组件中会通过 DataRouterStateContext.Provider 将最新的 router state 传递给子组件中。

如何使用 Router 为你页面带来更快的加载速度

因为我们的应用程序都是被 RouterProvider 包裹,自然当我们调用 useLoaderData 时只需要通过 context 的形式即可在组件中获得最新的 state 。

如何使用 Router 为你页面带来更快的加载速度

这一步整个流程就变的清晰了,当页面路由改变时

  1. 触发 startNavigation 寻找当前匹配的 route 对象。
  2. 执行当前匹配 route 对象的 loaderFunction 获得返回值。
  3. startNavigation 执行完成后会调用 completeNavigation 更新 router 的 state。
  4. RouterProvider 中由于 subscriber 了 router state 的变化,自然 RouterProvider 也会同步更新当前组件顶层的 state,同时通过 provider 的方式传递给所有子组件最新的 state。
  5. 最后,当我们在组件中调用 useLoaderData 时,由于 provider 中的 value 发生变化,useLoaderData 也会获得最新的 loaderData。

Defer & Await

了解了 ReactRouter 中 loader 是如何被调用以及如何将 loaderData 关联到页面数据上后我们来看看 defer 的大致实现过程。

Defer

其实 defer 的实现 ReactRouter 做了非常多的边界 case ,比如在页面快速切换时取消上一次的 defer Promise 等等之类的边界判断。

这里我们仅仅关心正常的 defer 是如何被执行的,关注一个大概的执行流程即可。有兴趣的同学可以自行翻阅 ReactRouter 的源代码去向详细阅读了解。

首先 defer 的存放位置在 packages/router/utils.ts 中:

如何使用 Router 为你页面带来更快的加载速度

我们可以看到 defer 方法返回的是一个 DeferredData 的实例:

如何使用 Router 为你页面带来更快的加载速度

DeferredData 这个类中,在初始化时会为 defer 包裹的对象中每个值调用 trackPromise 方法:

如何使用 Router 为你页面带来更快的加载速度

trackPromise 方法会为 defer 中的每个值标记 _tracked 为 true 表示该 Promise 已经被 ReactRouter 追踪。

同时 trackPromise 会返回返回一个新的 Promise:

如何使用 Router 为你页面带来更快的加载速度

abortPromise 表示 ReactRouter 中取消 defer 请求的逻辑,我们暂时无需关注它。

重点在于,当 defer 中的 promise 完成/失败后都会调用 this.onSettle 方法:

如何使用 Router 为你页面带来更快的加载速度

onSettle 方法会为 defer 方法中每个 promise 的值在 fulfilled 后根据返回的 data/error 标记对应的 _error 以及 _data 属性分别为错误信息/resoved 的值。

所以,简单来说 defer 方法会为包裹的 object 中每个值分别打上 tag 带上 _tracked 以及在 Promise 变为完成态后为 promise 标记 _data 或者 _error 属性。

Await

defer 往往是需要使用 Await 来配合使用。

如何使用 Router 为你页面带来更快的加载速度

Await 的实现就稍微显得简单了些,首先我们在看看 Await 组件中的 AwaitErrorBoundary 他会接受外部传入的 resolve ,通常这个 resolve 会是 useLoaderData 获取的 defer 方法返回的 promise。

如何使用 Router 为你页面带来更快的加载速度

它的实现就类似于我们通常使用的 ErrorBoundary 组件,AwaitErrorBoundary 组件的 render 函数中会首先获取到外部传入的 resolve 。

如果当前 resolve 已经被标记 ReactRouter 追踪(_tracked 为 true),那么此时会根据 _data/_error 来判断该 Promise 的状态:

如何使用 Router 为你页面带来更快的加载速度

  • Promise 拥有 _data 表示正常完成状态,正常完成状态时标记 AwaitRenderStatus 为成功。
  • Promise 拥有 _error 时候为完成(rejected)状态,标记状态为失败。
  • 否则,会标记状态为 pending ,同时在 render 中 throw 该 promise。

如何使用 Router 为你页面带来更快的加载速度

在成功和失败状态下 render 方法一目了然,当失败时会渲染 AwaitContext.Provider 传入当前 promise,同时将 children 重制为 errorElement。

当成功时,会正常将 children 传入为外部的 children。

自然,由于 pendding 状态的 Promise 会向外 throw promise ,我们在使用 Await 组件时需要配合 Suspense 组件。

由于我们在子组件(Await) 中 throw 出了当前 Promise,Supense 对于子组件会开启 fallback 进行异步加载等待 Promise 完成后又会更新状态重新渲染子组件(reRender 时 Await 中的 primise 已经不为 pendding,自然就会进入成功/失败的渲染逻辑了)。

如何使用 Router 为你页面带来更快的加载速度

之后,在了解了 AwaitErrorBoundary 的逻辑后,我们再来回到 ResolveAwait 组件:

如何使用 Router 为你页面带来更快的加载速度

ResolveAwait 做的事情也非常简单,判断传入的 children 是否为 function,如果为 function 的情况会调用 useAsyncValue 获取到 AwaitContext.Provider 的值调用该函数,如果不为函数则会直接渲染该组件(我们需要在组件内部自己通过 useAsyncvalue 获取外层 reolve 的返回值)。

这也是 ReactRouter Await Children 组件的两种传入方式,具体可以参考 文档说明

如何使用 Router 为你页面带来更快的加载速度

而所谓的 useAsyncValue 自然就是对应了 AwaitContext.Provider 传入的 value,获取了 primise._data 从而获取到 promise resolve 的值。

当然,与 useAsyncValue 对应的也存在 useAsyncError ,我们可以在 errorElement 中通过 useAsyncError 获取 promise rejected 的原因。

到这里,defer、Await 的执行流程我们就大概理清楚了:

  • 在 loader 中会通过调用 derfer 为每个 value 打上标记,标记 tracked 为 true,同时会为每个 Promise 在 reject/resolve 后为当前 promise 标记 _error/_data 为失败的原因/成功的数据。

  • 当我们在组件中使用 useLoaderData 获取到 defer 返回的数据时,对于每一个 value 需要通过 Suspense/Await 组件进行包裹使用。

  • Await 组件中会根据 promise 的 _tracked/_error/_data 判断当前 Promise 的状态从而进行不同 UI 的渲染。

Server Side Render

服务端 ReactRouter 简介

上述过程中对于 ReactRouter V6.4 新增的 data apis 的原理进行了浅析,我们了解到了在客户端执行时 data apis 的执行过程。

不过,上边的流程仅仅是针对于客户端渲染,也就是我们通常的 CSR 过程。

在 SSR 服务端渲染下,其实还是有非常多的不同的,比如通常在服务端中我们会在 createStateRouter 来处理服务端路由。

从而让路由的 loader 不会打包进入客户端代码,而是仅在我们的 Server 上运行 loaderFunction。

每次页面请求到来时,服务端会同步执行 React 组件渲染以及在服务端执行 loaderFunction ,客户端完全不进行任何 Loader 的感知。

如何使用 Router 为你页面带来更快的加载速度

在 createRouter 方法中如果存在 hydrationData 的话首次渲染是会标记 initialized 为 true 的。

我们刚才也提高过,如果 hydrationData 为 true 时,是不会在初始化时调用 startNavigation 的,自然也不会触发 laoder 的运行。

如何使用 Router 为你页面带来更快的加载速度

所谓的 hydrationData 及时在 SSR 时服务端数据和客户端数据交互的桥梁,ReactRouter 默认会通过 __staticRouterHydrationData 在 window 上传递。

当然,服务端渲染的 ReactRouter 又是另一个话题了。如果你有服务端需求强烈建议大家可以尝试下 Remix

Remix 中已经通过了一系列封装来为我们提供开箱即用的 ReactRouter Server Side Render。

Remix Defer

关于 Remix 在服务端渲染时做了许多构建相关的处理,简单来说他会在服务端构建时确定好每个路由需要的静态资源列表,说实话我也没看完这部分,笔者这里就不再展开了。

唯一想提到的就是上文我们说过,我们可以在客户端通过 defer 返回的对象中使用 Promise 来延迟我们部分页面的加载。

同时,我们也提到过在服务端渲染时通常 loaderFunction 并不会在客户端执行,而是在服务器上执行当前路由对应的 loaderFunction。

那么,如果我们通过 streaming 配合 defer 使用时,不知道大家有没有想过 Remix 是如何格式化服务端 loaderFunction 的 defer 呢?

上述这么说可能有些抽象,有些同学不了解 remix 可能并不清楚我在说什么。

简单来说 Remix 会在服务器上执行 loaderFunction,如果 loaderFunction 中返回 defer 的 promise,比如:

const fetchPromiseData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({});
    }, 10000);
  });
};
// 当前路由的 loaderFunction
export const loader: LoaderFunction = async ({ request, context }) => {
  return defer({
    data: fetchPromiseData(),
  });
};

remix 会在服务端执行 loader ,然后将服务端 pendding 状态的 promise 传递给客户端,客户端会判断服务端 Promise 状态:

  • pendding 时,渲染 Suspense 中的 fallbackElement。
  • resolve 时,会渲染 Await 组件的 children 同时获取 promise 的数据。
  • rejected 时,会渲染 errorElement。

一切看起来都和客户端一模一样,不过重点就在于 Remix 将服务端 loaderFunction 中 defer 返回的 Promise 序列化后返回给客户端,客户端也会得到这部分序列化后的 Promise ,听起来非常神奇对吧。

如果你直接使用 ReactRouter 作为你的服务端渲染应用,这部分 Promise 的序列化是需要你自己进行实现的。

如何使用 Router 为你页面带来更快的加载速度

实际上这部分 Promise 的序列化是在 Remix 的 <Scripts /> 组件中实现的:

如何使用 Router 为你页面带来更快的加载速度

在页面初始化渲染时,借助 <Await />__remixContext 的自定义 api 来实现了类似序列化的 Promise 在 Server 和 Client 中来维持相同状态通知。

具体 Remix 的相关 Defer 解读这里我就不再展开了,有兴趣的同学我们可以在评论区一起交流。

如果大家 Remix 有兴趣的话,之后我也会为大家带来 Remix 的文章分享。

写在结尾

文章中为大家分享了 React Data Apis 的优势、用法以及原理浅析,希望文章中的内容可以帮助到大家。