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 应用程序的最佳方式 😉。