React Router 中最基本的服务器端渲染非常简单。但是,除了让正确的路由渲染之外,还有很多需要考虑的事情。以下是一些你需要处理的事情的非完整列表
将所有这些设置好可能非常复杂,但值得你付出努力,因为只有在服务器端渲染时才能获得性能和 UX 特性。
如果你想服务器端渲染你的 React Router 应用程序,我们强烈建议你使用 Remix。这是我们另一个基于 React Router 构建的项目,它处理了上面提到的所有事情,甚至更多。试试看吧!
如果你想自己处理,你需要在服务器上使用 <StaticRouterProvider> 或 <StaticRouter>,具体取决于你选择的 路由器。如果使用 <StaticRouter>,请跳到 没有数据路由器 部分。
首先,你需要为数据路由器定义你的路由,这些路由将在服务器和客户端上使用
const React = require("react");
const { json, useLoaderData } = require("react-router-dom");
const routes = [
{
path: "/",
loader() {
return json({ message: "Welcome to React Router!" });
},
Component() {
let data = useLoaderData();
return <h1>{data.message}</h1>;
},
},
];
module.exports = routes;
esbuild、vite 或 webpack。
定义好路由后,我们可以在 Express 服务器中创建一个处理程序,并使用 createStaticHandler() 为路由加载数据。请记住,数据路由器的主要目标是将数据获取与渲染分离,因此你会看到,在使用数据路由器进行服务器端渲染时,我们有不同的步骤来获取和渲染数据。
const express = require("express");
const {
createStaticHandler,
} = require("react-router-dom/server");
const createFetchRequest = require("./request");
const routes = require("./routes");
const app = express();
let handler = createStaticHandler(routes);
app.get("*", async (req, res) => {
let fetchRequest = createFetchRequest(req, res);
let context = await handler.query(fetchRequest);
// We'll tackle rendering next...
});
const listener = app.listen(3000, () => {
let { port } = listener.address();
console.log(`Listening on port ${port}`);
});
请注意,我们必须首先将传入的 Express 请求转换为 Fetch 请求,这是静态处理程序方法所操作的请求。createFetchRequest 方法特定于 Express 请求,在本示例中是从 @remix-run/express 适配器中提取的
module.exports = function createFetchRequest(req, res) {
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();
res.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);
};
通过执行所有匹配的路由加载器来加载数据后,我们使用 createStaticRouter() 和 <StaticRouterProvider> 渲染 HTML 并将响应发送回浏览器
app.get("*", async (req, res) => {
let fetchRequest = createFetchRequest(req, res);
let context = await handler.query(fetchRequest);
let router = createStaticRouter(
handler.dataRoutes,
context
);
let html = ReactDOMServer.renderToString(
<StaticRouterProvider
router={router}
context={context}
/>
);
res.send("<!DOCTYPE html>" + html);
});
renderToString 来简化操作,因为我们已经在 handler.query 中加载了数据,并且在本示例中没有使用任何流式功能。如果你需要支持流式功能,则需要使用 renderToPipeableStream。
如果你希望支持 defer,你还需要管理将服务器端 Promise 序列化到客户端(提示,只需使用 Remix,它通过 Scripts 组件为你处理了这一点 😉)。
将 HTML 发送回浏览器后,我们需要使用 createBrowserRouter() 和 <RouterProvider> 在客户端“水化”应用程序
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import { routes } from "./routes";
let router = createBrowserRouter(routes);
ReactDOM.hydrateRoot(
document.getElementById("app"),
<RouterProvider router={router} />
);
这样你就拥有了一个服务器端渲染和水化的应用程序!有关工作示例,你也可以参考 Github 存储库中的 示例。
如上所述,服务器端渲染在大规模和生产级应用程序中很棘手,我们强烈建议你查看 Remix,如果这是你的目标。但如果你选择手动操作,以下是一些你可能需要考虑的其他概念
服务器端渲染的核心概念是 水化,它涉及将客户端 React 应用程序“附加”到服务器端渲染的 HTML。为了正确执行此操作,我们需要在服务器渲染期间创建客户端 React Router 应用程序,使其处于与服务器渲染期间相同的状态。当你的服务器渲染通过 loader 函数加载数据时,我们需要将此数据发送上来,以便我们可以使用相同的加载器数据创建客户端路由器,以进行初始渲染/水化。
本指南中显示的 <StaticRouterProvider> 和 createBrowserRouter 的基本用法会为你内部处理此操作,但如果你需要控制水化过程,可以通过 <StaticRouterProvider hydrate={false} /> 禁用自动水化过程。
在一些高级用例中,您可能希望部分地水化客户端 React Router 应用程序。您可以通过传递给 createBrowserRouter 的 future.v7_partialHydration 标志来实现这一点。
如果任何加载器重定向,handler.query 将直接返回 Response,因此您应该检查它并发送重定向响应,而不是尝试渲染 HTML 文档。
app.get("*", async (req, res) => {
let fetchRequest = createFetchRequest(req, res);
let context = await handler.query(fetchRequest);
if (
context instanceof Response &&
[301, 302, 303, 307, 308].includes(context.status)
) {
return res.redirect(
context.status,
context.headers.get("Location")
);
}
// Render HTML...
});
如果您在路由中使用 route.lazy,那么在客户端上,您可能拥有水化所需的所有数据,但您还没有路由定义!理想情况下,您的设置将在服务器上确定匹配的路由,并在关键路径上提供其路由包,这样您就不会在最初匹配的路由上使用 lazy。但是,如果情况并非如此,您需要加载这些路由并在水化之前更新它们,以避免路由回退到加载状态。
// Determine if any of the initial routes are lazy
let lazyMatches = matchRoutes(
routes,
window.location
)?.filter((m) => m.route.lazy);
// Load the lazy matches and update the routes before creating your router
// so we can hydrate the SSR-rendered content synchronously
if (lazyMatches && lazyMatches?.length > 0) {
await Promise.all(
lazyMatches.map(async (m) => {
let routeModule = await m.route.lazy();
Object.assign(m.route, {
...routeModule,
lazy: undefined,
});
})
);
}
let router = createBrowserRouter(routes);
ReactDOM.hydrateRoot(
document.getElementById("app"),
<RouterProvider router={router} fallbackElement={null} />
);
另请参阅
首先,您需要某种“应用程序”或“根”组件,该组件在服务器和浏览器中渲染。
export default function App() {
return (
<html>
<head>
<title>Server Rendered App</title>
</head>
<body>
<Routes>
<Route path="/" element={<div>Home</div>} />
<Route path="/about" element={<div>About</div>} />
</Routes>
<script src="/build/client.entry.js" />
</body>
</html>
);
}
这是一个在服务器上渲染应用程序的简单 express 服务器。请注意 StaticRouter 的使用。
import express from "express";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import App from "./App";
let app = express();
app.get("*", (req, res) => {
let html = ReactDOMServer.renderToString(
<StaticRouter location={req.url}>
<App />
</StaticRouter>
);
res.send("<!DOCTYPE html>" + html);
});
app.listen(3000);
renderToString,因为在这个简单的示例中我们没有使用任何流式功能。如果您需要支持流式功能,则需要使用 renderToPipeableStream。
最后,您需要一个类似的文件来使用您的 JavaScript 包“水化”应用程序,该包包含相同的 App 组件。请注意 BrowserRouter 而不是 StaticRouter 的使用。
import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
ReactDOM.hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.documentElement
);
客户端条目中唯一真正的区别是
StaticRouter 而不是 BrowserRouter<StaticRouter url>ReactDOMServer.renderToString 而不是 ReactDOM.render。您需要自己完成某些部分才能使其正常工作
<App> 组件中 <script> 的客户端条目在哪里。<title>)。再说一次,我们建议您看看 Remix。这是服务器渲染 React Router 应用程序的最佳方式——也许也是构建任何 React 应用程序的最佳方式 😉。