React Router v5 伴随着代码拆分和使用服务器端渲染的数据预取

Posted

技术标签:

【中文标题】React Router v5 伴随着代码拆分和使用服务器端渲染的数据预取【英文标题】:React Router v5 accompanied with Code Splitting, and Data Prefetching with the use of Server Side Rendering 【发布时间】:2021-07-24 20:47:08 【问题描述】:

我有一个项目使用 react-router v3 仅出于一个原因。原因是需要使用数据预取进行服务器端渲染,最方便的方法是将 集中式路由配置 保存在 objectarray 中并循环匹配元素以获取来自服务器端 API 的数据。稍后的数据将与响应 html 一起传递给客户端,并存储在 JSON 格式字符串的变量中。

应用程序也使用代码拆分,但是在服务器端使用 babel-plugin-transform-ensure-ignore 我可以直接获取组件而不是延迟加载,并且本机 import 方法将仅在客户端使用一边。

尽管如此,上述结构不适用于 react-router v5,因为它有点困难,因为我不能使用 @loadable/components,作为 react-router 官方文档建议。根据我的观察,@loadable/components 只是在服务器端生成 HTML,而不是给我实现负责服务器端逻辑的 fetch 方法的组件。

所以想请教一下webpack + react-router v5 + s-s-r + 数据预取 + redux + 代码拆分的好结构

我认为这很复杂,没有通用的解决方案,但是我可能错了。

感谢任何方向或建议。

【问题讨论】:

【参考方案1】:

我从未尝试过@loadable/components,但我使用自定义的代码拆分实现做了类似的事情(s-s-r + 代码拆分 + 数据预取),我相信你应该改变你的数据预取方法。

如果我没听错,你的问题是你试图干预正常的 React 渲染过程,提前推断出你的渲染中将使用哪些组件,因此应该预取哪些数据。这种干预/推论并不是 React API 的一部分,虽然我看到不同的人使用一些未记录的内部 React 东西来实现它,但从长远来看,它们都很脆弱,并且容易出现像你这样的问题。

我相信,一个更好的防弹方法是将 s-s-r 执行为几个正常的渲染通道,在每个通道中收集要预取的数据列表,获取它们,然后从最开始重复渲染从更新状态开始。我正在努力想出一个明确的解释,但让我试试这样的例子。

比如说,你的应用树中某处的组件 <A> 依赖于异步获取的数据,这些数据应该存储在 Redux 存储的 some.path 中。考虑一下:

    假设您从空的 Redux 存储开始,并且您还拥有 s-s-r 上下文(为此您可以重用 StaticRouter 的 context,或者使用 React 的 Context API 创建一个单独的)。 您使用ReactDOMServer.renderToString(..) 对整个应用执行非常基本的 s-s-r。 当渲染器到达并渲染组件 <A> 在应用程序树中的某个位置时,无论它是否被代码拆分,如果一切设置正确,该组件都可以访问 Redux 存储,并且到 s-s-r 上下文。因此,如果<A> 看到当前渲染发生在服务器上,并且没有数据预取到 Redux 存储的 some.path<A> 将保存到 s-s-r 上下文中“加载这些数据的请求”,并渲染一些占位符(或者在没有预取这些数据的情况下渲染任何有意义的东西)。我所说的“加载这些数据的请求”是指<A> 实际上可以触发一个异步函数,该函数将获取数据,并将相应的数据承诺推送到上下文中的专用数组。 一旦ReactDOMServer.renderToString(..) 完成,您将拥有:呈现的HTML 标记的当前版本,以及收集在s-s-r 上下文对象中的一组数据获取承诺。您可以在此处执行以下操作之一: 如果没有将 Promise 收集到 s-s-r 上下文中,那么您呈现的 HTML 标记是最终的,您可以将它与 Redux 存储内容一起发送到客户端; 如果有待处理的 Promise,但 s-s-r 已经花费了太长时间(从 (1) 开始计算),您仍然可以发送当前 HTML 和当前 Redux 存储内容,并仅依靠客户端获取任何丢失的数据,然后完成渲染(从而在服务器延迟和 s-s-r 完整性之间进行折衷)。 如果可以等待,则等待所有待处理的承诺;将所有获取的数据添加到 Redux 存储的正确位置;重置 s-s-r 上下文;然后返回 (2),从头开始重复渲染,但使用更新的 Redux 存储内容。

您应该看到,如果正确实施,它将与依赖异步数据的任意数量的不同组件一起工作,无论它们是否嵌套,以及您如何准确地实现代码拆分、路由等 /em> 重复渲染过程会产生一些开销,但我认为这是可以接受的。


一个小代码示例,基于我使用的代码片段:

s-s-r 循环 (original code):

const s-s-rContext = 
  // That's the initial content of "Global State". I use a custom library
  // to manage it with Context API; but similar stuff can be done with Redux.
  state: ,
;

let markup;
const s-s-rStart = Date.now();
for (let round = 0; round < options.maxSsrRounds; ++round) 
  // These resets are not in my original code, as they are done in my global
  // state management library.
  s-s-rContext.dirty = false;
  s-s-rContext.pending = [];

  markup = ReactDOM.renderToString((
    // With Redux, you'll have Redux store provider here.
    <GlobalStateProvider
      initialState=s-s-rContext.state
      s-s-rContext=s-s-rContext
    >
      <StaticRouter
        context=s-s-rContext
        location=req.url
      >
        <App />
      </StaticRouter>
    </GlobalStateProvider>
  ));

  if (!s-s-rContext.dirty) break;

  const timeout = options.s-s-rTimeout + s-s-rStart - Date.now();
  const ok = timeout > 0 && await Promise.race([
    Promise.allSettled(s-s-rContext.pending),
    time.timer(timeout).then(() => false),
  ]);
  if (!ok) break;

  // Here you should take data resolved by "s-s-rContext.pending" promises,
  // and place it into the correct paths of "s-s-rContext.state", before going
  // to the next s-s-r iteration. In my case, my global state management library
  // takes care of it, so I don't have to do it explicitly here.

// Here "s-s-rContext.state" should contain the Redux store content to send to
// the client side, and "markup" is the corresponding rendered HTML.

而依赖异步数据的组件内部的逻辑会有点像这样:

function Component() 
  // Try to get necessary async from Redux store.
  const data = useSelector(..);

  // react-router does not provide a hook for accessing the context,
  // and in my case I am getting it via my <GlobalStateProvider>, but
  // one way or another it should not be a problem to get it.
  const s-s-rContext = useSsrContext();

  // No necessary data in Redux store.
  if (!data) 
    // We are at server.
    if (s-s-rContext) 
      s-s-rContext.dirty = true;
      s-s-rContext.pending.push(
        // A promise which resolves to the data we need here.
      );

    // We are at client-side.
     else 
      // Dispatch an action to load data into Redux store,
      // as appropriate for your setup.
    
  

  return data ? (
    // Return the complete component render, which requires "data"
    // for rendering.
  ) : (
    // Return an appropriate placeholder (e.g. a "loading" indicator).
  );

【讨论】:

嗨@Sergey,感谢您的回复。如果您能用小例子进一步解释,我将不胜感激。它不应该是完整的解决方案,也可以是一些图表。我认为这个问题的解决方案对其他人也很有价值。 嗨@ArenHovsepyan,我更新了我的答案,添加了两个代码片段。希望它能让事情变得更清楚。 嗨@Sergey,基本上我的理解是我们可以制作高阶函数并基于一些变量触发数据获取,对吧? 嗯...不确定,你在那里称什么为高阶函数?要点是你可以做一个非常简单的 s-s-r 渲染,所有组件都依赖于异步数据,将它们需要的数据写入上下文对象,然后等待这些数据,并使用更新的状态重做 s-s-r。 对于高阶函数,我的意思是 HOC - 高阶组件,它可以作为必须进行服务器端操作的参数组件和应该执行该操作的函数。因此,我们可以将可重用的 HOC 设为 connect,但出于此目的。

以上是关于React Router v5 伴随着代码拆分和使用服务器端渲染的数据预取的主要内容,如果未能解决你的问题,请参考以下文章

React Router v6 和 ow Params 不像 v5 那样工作

react-router路由之routerRender方法(v5 v6)

React开发中使用react-router-dom路由最新版本V5.1.2路由嵌套子路由

React开发中使用react-router-dom路由最新版本V5.1.2路由嵌套子路由

react-router-dom v5和react-router-dom v6区别

react-router-dom V5 使用指南