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

React SSR的基本原理都已经描述过了,在描述最后一个函数hydrateRoot()的时候,苏南大叔提到:“是不是会报错就无关该函数的使用了”。实际上说的就是同构的问题,其中包括"组件同构"以及"路由同构"。这些代码的复用,是保证SSR成功的关键因素之一,本文将详细展开描述。

苏南大叔:React SSR,服务器端和客户端,组件同构+路由同构 - 组件同构和路由同构
React SSR,服务器端和客户端,组件同构+路由同构(图4-1)

苏南大叔的“程序如此灵动”博客,记录苏南大叔的编程心得体会。本文测试环境:nodejs@20.18.0create-react-app@5.0.1react-router-dom@6.27.0react@18.3.1express@4.21.1。本文主要体现同构的概念,实际上就是多套不同的运行方式之间,如何复用关键代码。

前文回顾

React SSR,说到根本,就是把页面内容在服务器端生成一次,然后在客户端再绑定一次。这个过程,虽然有悖于react的传统理念。但是,和传统网页的写法,或者说SEO的理念是不谋而合的,下面先回顾一下往期内容。

脱水注水的三个关键函数:

不同的react路由方式概念:

不同的react路由定义方式:

服务器端软件express的使用方法:

苏南大叔:React SSR,服务器端和客户端,组件同构+路由同构 - 流程图
React SSR,服务器端和客户端,组件同构+路由同构(图4-2)

组件同构

实际上就是说,在执行脱水或者注水的时候,使用相同的组件(代码)。具体操作上来说,就是把每个组件都放置到独立的文件里面,(文件名后缀是个重要点,待议)。然后再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;

引入的时候,是在同构的路由文件里面引入的。所以请继续阅读。

苏南大叔:React SSR,服务器端和客户端,组件同构+路由同构 - post独立组件
React SSR,服务器端和客户端,组件同构+路由同构(图4-3)

组件的代码不重要,本文要表述的重点:是要把这个组件独立成单独文件,以便不同的场景进行调用。

苏南大叔:React SSR,服务器端和客户端,组件同构+路由同构 - 运行一致
React SSR,服务器端和客户端,组件同构+路由同构(图4-4)

路由同构之“Routes + Route”

因为路由的背后,就是一个一个的组件。实际上是利用路由把这些组件进行串联的。把组件都独立出去后,路由的代码也需要独立出去。

需要特别注意的是:BrowserRouterStaticRouter是区分服务器端路由还是客户端路由的重要标准。所以,它们必须出现在独立的同构路由定义文件外部。也就是说,被同构的路由配置信息里面,完全不可以体现使用的是何种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代码中那些奇奇怪怪的后缀名,待后续文章总结。

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

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

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

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