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

Are*_*yan 5 javascript lazy-loading reactjs webpack server-side-rendering

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

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

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

所以想请教一下webpack + react-router v5 + ssr + data prefetch + redux + code splitting的好结构

我看到它非常复杂并且没有通用的解决方案,但是我可能是错的。

任何方向或建议表示赞赏。

Ser*_*din 2

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

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

我相信,更好的防弹方法是将 SSR 作为一些正常的渲染过程来执行,在每个过程中收集要预取的数据列表,获取它们,然后从头开始重复渲染并更新状态。我正在努力想出一个清晰的解释,但让我尝试举这样的例子。

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

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

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


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

SSR循环(原始代码):

const ssrContext = {
  // 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 ssrStart = 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.
  ssrContext.dirty = false;
  ssrContext.pending = [];

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

  if (!ssrContext.dirty) break;

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

  // Here you should take data resolved by "ssrContext.pending" promises,
  // and place it into the correct paths of "ssrContext.state", before going
  // to the next SSR iteration. In my case, my global state management library
  // takes care of it, so I don't have to do it explicitly here.
}
// Here "ssrContext.state" should contain the Redux store content to send to
// the client side, and "markup" is the corresponding rendered HTML.
Run Code Online (Sandbox Code Playgroud)

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

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 ssrContext = useSsrContext();

  // No necessary data in Redux store.
  if (!data) {
    // We are at server.
    if (ssrContext) {
      ssrContext.dirty = true;
      ssrContext.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).
  );
}
Run Code Online (Sandbox Code Playgroud)