React SSR,服务器端和客户端,组件同构+路由同构
发布于 作者:苏南大叔 来源:程序如此灵动~React SSR
的基本原理都已经描述过了,在描述最后一个函数hydrateRoot()
的时候,苏南大叔提到:“是不是会报错就无关该函数的使用了”。实际上说的就是同构的问题,其中包括"组件同构"以及"路由同构"。这些代码的复用,是保证SSR
成功的关键因素之一,本文将详细展开描述。
苏南大叔的“程序如此灵动”博客,记录苏南大叔的编程心得体会。本文测试环境: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 SSR
,说到根本,就是把页面内容在服务器端生成一次,然后在客户端再绑定一次。这个过程,虽然有悖于react
的传统理念。但是,和传统网页的写法,或者说SEO
的理念是不谋而合的,下面先回顾一下往期内容。
脱水注水的三个关键函数:
- https://newsn.net/say/react-render-to-string.html
- https://newsn.net/say/react-render-to-pipeable-stream.html
- https://newsn.net/say/react-hydrate-root.html
不同的react
路由方式概念:
不同的react
路由定义方式:
- https://newsn.net/say/react-router-routes.html
- https://newsn.net/say/react-router-provider.html
- https://newsn.net/say/react-static-router-provider.html
服务器端软件express
的使用方法:
组件同构
实际上就是说,在执行脱水或者注水的时候,使用相同的组件(代码)。具体操作上来说,就是把每个组件都放置到独立的文件里面,(文件名后缀是个重要点,待议)。然后再import
到具体的业务场景里面去。
对应于苏南大叔的最近这些文章的例子里面的话,就是要把Post.jsx
这个组件给独立出来。下面的代码功用内容,其实并不重要。这里仅仅是展示一个代码的框架性写法而已。
src/Post.jsx
:
import React from "react";
import { useParams, Link } from "react-router-dom";
function Post() {
var { id } = useParams();
return (
<div>
<div>No.{id} article</div>
<div>
<Link to="/">Home</Link>
</div>
</div>
);
}
export default Post;
引入的时候,是在同构的路由文件里面引入的。所以请继续阅读。
组件的代码不重要,本文要表述的重点:是要把这个组件独立成单独文件,以便不同的场景进行调用。
路由同构之“Routes + Route”
因为路由的背后,就是一个一个的组件。实际上是利用路由把这些组件进行串联的。把组件都独立出去后,路由的代码也需要独立出去。
需要特别注意的是:BrowserRouter
和StaticRouter
是区分服务器端路由还是客户端路由的重要标准。所以,它们必须出现在独立的同构路由定义文件外部。也就是说,被同构的路由配置信息里面,完全不可以体现使用的是何种Router
。
src/config/MyRoute.jsx
:
import React from "react";
import { Routes, Route } from "react-router-dom";
import Post from "./../Post";
import { Link } from "react-router-dom";
export default () => {
return (
<Routes>
<Route path="/" element={<div><Link to="/post/123">Post</Link></div>} />
<Route path="/post/:id" element={<Post />} />
</Routes>
);
};
客户端使用的时候(createRoot()
或者hydrateRoot()
),App.js
:
import React from "react";
import { BrowserRouter } from "react-router-dom";
import RouteConfig from "./config/MyRoute";
function App() {
return (
<BrowserRouter>
<RouteConfig/>
</BrowserRouter>
);
}
export default App;
注意,createRoot()
或者hydrateRoot()
,存在于index.js
文件里面。所以,目前的App.js
,并不能看出来最终的使用目的。
服务器端使用的时候(renderToString()
或者renderToPipeableStream()
),server.jsx
:
import express from "express";
import React from "react";
// import { Routes, Route, useParams } from "react-router-dom";
import { StaticRouter } from "react-router-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import RouteConfig from "./src/config/MyRoute";
const app2 = express();
app2.use(express.static("dist"));
app2.get("/*", (req, res) => {
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">
<StaticRouter location={req.url}>
<RouteConfig></RouteConfig>
</StaticRouter>
</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);
路由同构之RouterProvider
【官方推荐】
对于另外的那种RouterProvider
+createXxxRouter()
的定义方法,同构的部分就仅仅是个数组了(这里是个小Trick
,以后用得到)。然后通过在非同构的地方,使用不同的createXxxRouter()
方法,变成真正的路由定义(也就是说,比上面的<Routes>
定义方式,多了一个文件步骤)。
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;
客户端使用的时候(createRoot()
或者hydrateRoot()
),App.js
:
import React from "react";
import RouteConfig from "./config/MyRoute";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter(RouteConfig);
function App() {
return <RouterProvider router={router}></RouterProvider>;
}
export default App;
服务器端使用的时候(renderToString()
或者renderToPipeableStream()
),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);
结束语
以此思路进行代码的同构的话,就会得到相对完美的项目。对于同构文件的后缀.jsx
,对于webpack
来说,这个意义不大,神马后缀都能识别。但是,对于esno server.jsx
来说,意义重大,不定义为.jsx
文件,会各种报错。代码的运行方式,可不是本文提及的这几种。方式很多,这里不必纠结。
所以,对于本文中的文件后缀问题,就看实际情况了,别纠结本文中的.jsx
后缀。关于react
代码中那些奇奇怪怪的后缀名,待后续文章总结。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。