React路由,解析StaticRouterProvider+createStaticRouter
发布于 作者:苏南大叔 来源:程序如此灵动~在前面的文章里面,苏南大叔总结到:react router
有两种使用方式,分别是:<Router>
+<Routes>
+<Route>
、createBrowserRouter()
+<RouterProvider>
。如果在服务器端使用react router
的话,BrowserRouter
或者HashRouter
,就会被更换为StaticRouter
。这就是本文写作的基本背景。
苏南大叔的“程序如此灵动”博客,记录苏南大叔的编程心得体会。本文测试环境:nodejs@20.18.0
,create-react-app@5.0.1
,react-router-dom@6.27.0
,react@18.3.1
,express@4.21.1
。
前文回顾
先回顾一下React
路由中的两大类定义方式:
Routes
+Route
,https://newsn.net/say/react-router-routes.htmlRouterProvider
,https://newsn.net/say/react-router-provider.html
然而,在服务器端,必须使用StaticRouter
来代替BrowserRoute
或者HashRouter
。参考:
- 服务器端StaticRouter,https://newsn.net/say/react-static-router.html
所以,上面几篇文章的推断就是:在服务器端,需要使用createStaticRouter()
来替代createBrowserRouter()
函数。本文的内容就是基于这个推断展开的。事实上,react-router-dom/server
还同时推出了个<StaticRouterProvider/>
来替代<RouterProvider/>
。
官方函数说明
这createStaticRouter()
一系列,使用起来相对复杂。一共涉及到四个函数,官方说明分别是:
createStaticHandler()
https://reactrouter.com/en/main/routers/create-static-handlercreateFetchRequest()
https://github.com/remix-run/expresscreateStaticRouter()
https://reactrouter.com/en/main/routers/create-static-router<StaticRouterProvider/>
https://reactrouter.com/en/main/routers/static-router-provider
这些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);
关键代码
本文的代码,主要改编自下面这篇文章:
其中,最核心的关键修改点,如下:
// ...
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
相关文章,请参考:
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。