我们相信:世界是美好的,你是我也是。平行空间的世界里面,不同版本的生活也在继续...

在前面的文章里面,苏南大叔总结到:react router有两种使用方式,分别是:<Router>+<Routes>+<Route>createBrowserRouter()+<RouterProvider>。如果在服务器端使用react router的话,BrowserRouter或者HashRouter,就会被更换为StaticRouter。这就是本文写作的基本背景。

苏南大叔:React路由,解析StaticRouterProvider+createStaticRouter - create-static-router
React路由,解析StaticRouterProvider+createStaticRouter(图2-1)

苏南大叔的“程序如此灵动”博客,记录苏南大叔的编程心得体会。本文测试环境:nodejs@20.18.0create-react-app@5.0.1react-router-dom@6.27.0react@18.3.1express@4.21.1

前文回顾

先回顾一下React路由中的两大类定义方式:

然而,在服务器端,必须使用StaticRouter来代替BrowserRoute或者HashRouter。参考:

所以,上面几篇文章的推断就是:在服务器端,需要使用createStaticRouter()来替代createBrowserRouter()函数。本文的内容就是基于这个推断展开的。事实上,react-router-dom/server还同时推出了个<StaticRouterProvider/>来替代<RouterProvider/>

官方函数说明

createStaticRouter()一系列,使用起来相对复杂。一共涉及到四个函数,官方说明分别是:

这些createStaticRouter()相关函数比其它的兄弟函数createBrowserRouter(),用起来要复杂的多。

createFetchRequest()函数引用

这个函数来自于@remix-run/express,常规来说是应该通过npm i @remix-run/express进行安装的。但是,安装失败了。所以,按照react router官方的提示,新建了只有这个导出函数的文件。

src/helpers/createFetchRequest.js

module.exports = function createFetchRequest(req) {
  let origin = `${req.protocol}://${req.get("host")}`;
  // Note: This had to take originalUrl into account for presumably vite's proxying
  let url = new URL(req.originalUrl || req.url, origin);
  let controller = new AbortController();
  req.on("close", () => controller.abort());
  let headers = new Headers();
  for (let [key, values] of Object.entries(req.headers)) {
    if (values) {
      if (Array.isArray(values)) {
        for (let value of values) {
          headers.append(key, value);
        }
      } else {
        headers.set(key, values);
      }
    }
  }
  let init = {
    method: req.method,
    headers,
    signal: controller.signal,
  };
  if (req.method !== "GET" && req.method !== "HEAD") {
    init.body = req.body;
  }
  return new Request(url.href, init);
};

然后import引入该模块:

import createFetchRequest from "./src/helpers/createFetchRequest";

独立route配置数组文件

createXxxRouter()函数系列里面,配置的路由信息,【都是数组!!!】。这里除了配置必要的path/element/children/index等等信息外,还可以配置自定义的信息(这是个官方没有提及的Trick,待续)。

这里使用了一个独立的src/config/MyRoute.jsx文件来导出这个路由信息数组,文件命名不解释。

import React from "react";
import { Link } from "react-router-dom";
import Post from "./../Post";
const routers = [
  {
    path: "/",
    element: (
      <div>
        <Link to="/post/123">Post</Link>
      </div>
    ),
  },
  {
    path: "/post/:id",
    element: <Post />,
  },
];
export default routers;

服务器端完整主体代码

server.jsx完整代码:

import express from "express";
import React from "react";
import {
  createStaticHandler,
  createStaticRouter,
  StaticRouterProvider,
} from "react-router-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import RouteConfig from "./src/config/MyRoute";
import createFetchRequest from "./src/helpers/createFetchRequest";
const app2 = express();
app2.use(express.static("dist"));
app2.get("/*", async (req, res) => {
  let { query, dataRoutes } = createStaticHandler(RouteConfig);
  let fetchRequest = createFetchRequest(req);
  let context = await query(fetchRequest);
  let router = createStaticRouter(dataRoutes, context);
  const App = function () {
    return (
      <html lang="en">
        <head>
          <meta charset="UTF-8" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0"
          />
          <meta http-equiv="X-UA-Compatible" content="ie=edge" />
          <title>React Server Side Rendering</title>
        </head>
        <body>
          <div id="root">
            <StaticRouterProvider
              router={router}
              context={context}
            ></StaticRouterProvider>
          </div>
        </body>
      </html>
    );
  };
  let data = {
    www: "newsn.net",
    author: "苏南大叔2",
  };
  data = JSON.stringify(data);
  const { pipe } = renderToPipeableStream(<App />, {
    bootstrapScriptContent: "window.__ROUTE_DATA__ = " + data,
    bootstrapScripts: ["/app.js"],
    onShellReady: () => {
      res.setHeader("content-type", "text/html");
      pipe(res);
    },
  });
});
app2.listen(3006);

苏南大叔:React路由,解析StaticRouterProvider+createStaticRouter - demo运行
React路由,解析StaticRouterProvider+createStaticRouter(图2-2)

关键代码

本文的代码,主要改编自下面这篇文章:

其中,最核心的关键修改点,如下:

// ...
let { query, dataRoutes } = createStaticHandler(RouteConfig);
let fetchRequest = createFetchRequest(req);
let context = await query(fetchRequest);
let router = createStaticRouter(dataRoutes, context);
const App = function () {
  return (<StaticRouterProvider
            router={router}
            context={context}
          ></StaticRouterProvider>);
};
//...

题外话

有关本文话题内容的题外话,有两个,具体待后续再开话题讨论。

额外的script

在查看源码的时候,偶然发现:<StaticRouterProvider>生成了下面的script内容:

window.__staticRouterHydrationData = JSON.parse("{\"loaderData\":{\"1\":null},\"actionData\":null,\"errors\":null}");

和自定义的类似代码,形成了对比:

window.__ROUTE_DATA__ = {"www":"newsn.net","author":"苏南大叔2"}

这个window.__staticRouterHydrationData如何使用和理解,待后续文章解释。

文件名后缀

仔细看的话,会发现本文的文件名后缀都是.jsx,而不是.js。这是因为会使用esno直接执行对应程序,这个是esno能成功执行这个主体文件,所必须的设定。

结束语

本篇文章很抽象,没有看懂前置阅读项目那几篇文章的读者,可能很难理解本篇文章到底发生了什么事情。更多苏南大叔的react相关文章,请参考:

如果本文对您有帮助,或者节约了您的时间,欢迎打赏瓶饮料,建立下友谊关系。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。

 【福利】 腾讯云最新爆款活动!1核2G云服务器首年50元!

 【源码】本文代码片段及相关软件,请点此获取更多信息

 【绝密】秘籍文章入口,仅传授于有缘之人   react