深入理解 Server-Side Rendering 的核心原理与实现细节
Next.js 虽好,但在大型项目中可能面临以下问题:
服务端渲染的核心价值:
matchRoutes() 解析 URL
prefetchQuery + dehydrate
renderToPipeableStream
hydrateRoot
Koa Router 拦截所有请求,进行 SSR 渲染
app/server/index.tsxconst router = new Router();
router.get("(.*)", async (ctx) => {
// 1. 创建 ChunkExtractor 追踪代码分割
const extractor = new ChunkExtractor({ statsFile });
// 2. 创建 QueryClient 用于数据预取
const queryClient = new QueryClient();
// 3. 数据预取(关键步骤!)
const { dehydratedState } = await prefetch(ctx, queryClient);
// 4. 渲染 React 应用
const { jsx, helmetContext } = renderApp(ctx, queryClient, extractor, dehydratedState);
// 5. 流式响应
await response(ctx, jsx, { helmetContext, extractor }, { dehydratedState });
});
使用 React Query 在服务端预取数据,然后序列化传递给客户端
app/server/app.tsxexport const prefetch = async (ctx, queryClient) => {
// 1. 匹配当前 URL 对应的路由
const prefetchRoutes = matchRoutes(routes, ctx.req.url);
// 2. 遍历匹配的路由,执行数据预取
const promiseRoutes = prefetchRoutes.map(({ route, params }) => {
if (route?.queryKey && route?.loadData) {
return queryClient.prefetchQuery({
queryKey: route.queryKey,
queryFn: () => route.loadData(params),
});
}
});
await Promise.allSettled(promiseRoutes);
// 3. 脱水:将数据序列化为 JSON
return { dehydratedState: dehydrate(queryClient) };
};
Dehydrate(脱水):将服务端的数据状态序列化为
JSON 字符串
Hydrate(水合):客户端读取 JSON
数据,恢复应用状态,绑定事件
使用 React 18 的 renderToPipeableStream 实现流式 HTML 响应
app/server/stream/response.tsimport { renderToPipeableStream } from "react-dom/server";
export const response = (ctx, jsx, startTemplate, endTemplate) => {
return new Promise((resolve, reject) => {
const { pipe } = renderToPipeableStream(jsx, {
// Shell 准备就绪时发送 HTML 头部
onShellReady() {
ctx.res.write(getStartTemplate(startTemplate));
},
// 所有内容就绪后发送完整响应
onAllReady() {
pipe(ctx.res); // 管道输出 React 渲染内容
ctx.res.end(getEndTemplate(endTemplate));
},
});
});
};
读取服务端注入的数据,进行水合,支持 CSR 降级
app/client/index.tsx// 1. 从 HTML 中读取服务端预取的数据
const getDehydratedState = () => {
const element = document.querySelector("#__REACT_QUERY_STATE__");
return JSON.parse(element.textContent);
};
// 2. 检测是否是 SSR 模式
const tradeFlag = getTradeFlag(); // { isSSR: true/false }
// 3. 根据模式选择渲染方式
if (tradeFlag.isSSR) {
loadableReady(() => {
hydrateRoot(root, <ClientApp />); // SSR 水合
});
} else {
createRoot(root).render(<ClientApp />); // CSR 降级
}
当服务端渲染失败时,客户端会自动降级为 CSR 模式,保证应用可用性。这是自定义 SSR 相比 Next.js 的优势之一!
const routes: PreFetchRouteObject[] = [
{
path: "/",
element: <Index />,
children: [
{
path: ":locales/home",
element: <Home />,
// 👇 数据预取配置
queryKey: [PrefetchKeys.HOME], // React Query 缓存键
loadData: HomeService.getList, // 数据获取函数
},
],
},
];
每个路由可以定义 queryKey 和 loadData,服务端会在渲染前自动调用这些函数预取数据。
| 层级 | 技术 | 用途 |
|---|---|---|
| 前端框架 | React 19 | UI 渲染,支持最新的 Concurrent 特性 |
| 服务端 | Koa.js | HTTP 服务,中间件管理 |
| 数据管理 | React Query (TanStack) | 数据预取、缓存、状态管理 |
| 路由 | React Router DOM v7 | 客户端/服务端通用路由 |
| 代码分割 | @loadable/component | 路由级别的懒加载 |
| SEO | react-helmet-async | 服务端 TDK 注入 |
| 构建 | Webpack 5 | 双入口打包(client/server) |
| Serverless | AWS Lambda / Cloudflare Workers | 无服务器部署 |
# 安装依赖
pnpm install
# 启动开发服务器
pnpm run dev
# 启动 Mock 服务(可选)
pnpm run mock
# 本地构建
pnpm run build
# 线上构建
pnpm run build:online
# 启动生产服务(PM2)
pnpm run start
同一份代码在服务端和客户端运行,StaticRouter (SSR) ↔ BrowserRouter (CSR)
Dehydrate/Hydrate 模式确保服务端预取的数据无缝传递给客户端
renderToPipeableStream 实现 HTML 流式响应,提升 TTFB
SSR 失败时自动降级到 CSR,保证应用稳定性