React Router 中最基本的伺服器渲染非常直接。但是,需要考慮的因素遠不止讓正確的路由渲染。以下是不完整的清單,其中包含您需要處理的事項
良好地設定所有這些可能很複雜,但這些效能和 UX 特性只有在伺服器渲染時才可獲得,這些效能和 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
,您還需要管理伺服器端承諾的序列化,並從網路傳送到客戶端(提示:直接使用 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,如果那是您的目標。但是,如果您想要親自操作,這裡有幾個可能會需要考慮的其他概念
伺服器側渲染的核心概念是Hydration (水化),它包含了「附加」客戶端 React 應用程式到伺服器渲染的 HTML 中。要正確執行這項工作,我們需要在與伺服器渲染過程中相同的狀態下,建立我們的客戶端 React Router 應用程式。當您的伺服器渲染透過 loader
函數載入資料時,我們需要發送此資料,以便我們能為初始渲染/水化建立具有相同 loader 資料的客戶端路由器。
這份指南中展示的 <StaticRouterProvider>
和 createBrowserRouter
的基本用法會在內部為您處理這件事,但如果您需要控制水化程序,您可以透過 <StaticRouterProvider hydrate={false} />
來停用自動水化程序。
在一些進階使用案例中,您可能會想要部分水化客戶端 React Router 應用程式。您可以透過傳遞給 createBrowserRouter
的 future.v7_partialHydration
標誌來執行此操作。
如果任何 loader 重新導向,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} />
);
另請參閱
首先,您需要某種會在伺服器和瀏覽器中渲染的「app」或「root」組件
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 伺服器,它會在伺服器上渲染 app。請注意使用 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
。
最後,您需要類似的檔案來「水化」包含完全相同的 App
組件的 JavaScript 套件。請注意使用 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 app 進行伺服器渲染的最佳方式,或許也是建置任何 React app 的最佳方式 😉。