🚀 React 自定义 SSR 工程架构解析

深入理解 Server-Side Rendering 的核心原理与实现细节

React 19 Koa.js React Query Webpack 5

项目概述

🎯 为什么要自定义 SSR?

Next.js 虽好,但在大型项目中可能面临以下问题:

  • 性能问题:冷启动时间可达 4S+
  • 稳定性:不支持服务降级到 CSR
  • 安全性:存在多个 CVE 漏洞
  • 可控性:闭源趋势,高级功能绑定 Vercel

SSR 解决了什么问题?

服务端渲染的核心价值:

  • 性能优化:首屏渲染更快,FCP 更短
  • SEO 友好:搜索引擎可直接抓取内容
  • 用户体验:无白屏,渐进式加载

核心架构图

SSR Architecture

📊 架构图示意(如图片未加载)

[ Koa Server ] ↔ [ React App ] ↔ [ Browser Client ]

SSR 渲染流程

1

路由匹配

matchRoutes() 解析 URL

2

数据预取

prefetchQuery + dehydrate

3

流式渲染

renderToPipeableStream

4

客户端水合

hydrateRoot

Data Flow

项目文件结构

  • react-custom-ssr/
    • app/ (核心应用代码)
      • client/ - 客户端入口
        • index.tsx - 水合逻辑
      • server/ - 服务端入口
        • index.tsx - Koa 路由
        • app.tsx - 渲染逻辑
        • htmlTemplate.ts - HTML 模板
        • stream/ - 流式响应
      • utils/ - 工具函数
    • src/ (业务代码)
      • index.tsx - React App 入口
      • routes/ - 路由配置
      • pages/ - 页面组件
      • apis/ - API 接口
    • config/ - Webpack 配置
      • webpack.config.js
      • webpack.dev.js
      • webpack.prod.js
    • build/ - 构建产物
File Structure

核心代码解析

📦 1. 服务端路由处理 app/server/index.tsx

Koa Router 拦截所有请求,进行 SSR 渲染

app/server/index.tsx
const 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 });
});

🔄 2. 数据预取机制 app/server/app.tsx

使用 React Query 在服务端预取数据,然后序列化传递给客户端

app/server/app.tsx
export 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 & Hydrate

Dehydrate(脱水):将服务端的数据状态序列化为 JSON 字符串
Hydrate(水合):客户端读取 JSON 数据,恢复应用状态,绑定事件

🌊 3. 流式渲染 app/server/stream/response.ts

使用 React 18 的 renderToPipeableStream 实现流式 HTML 响应

app/server/stream/response.ts
import { 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));
      },
    });
  });
};

💧 4. 客户端水合 app/client/index.tsx

读取服务端注入的数据,进行水合,支持 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 降级机制

当服务端渲染失败时,客户端会自动降级为 CSR 模式,保证应用可用性。这是自定义 SSR 相比 Next.js 的优势之一!

路由与数据预取配置

🛤️ 路由配置示例

src/routes/index.tsx
const routes: PreFetchRouteObject[] = [
  {
    path: "/",
    element: <Index />,
    children: [
      {
        path: ":locales/home",
        element: <Home />,
        // 👇 数据预取配置
        queryKey: [PrefetchKeys.HOME],  // React Query 缓存键
        loadData: HomeService.getList,  // 数据获取函数
      },
    ],
  },
];

每个路由可以定义 queryKeyloadData,服务端会在渲染前自动调用这些函数预取数据。

技术栈一览

层级 技术 用途
前端框架 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,保证应用稳定性