黑白梦黑白梦

  • 文章
  • 专栏
  • 文章
  • 专栏
全部文章

React Router v7 SSR 使用笔记

发布于 2026-08-23约 16 分钟

React Router 在 v7 版本中融合了 Remix 的全栈能力,演进为支持服务端渲染的全栈 Web 框架。它全面基于 Web Standard API(Request / Response / FormData)构建,内置了数据加载与突变状态机、全链路类型自动生成以及嵌套路由体系。

初始化与 SSR 配置

配置 React Router v7 项目并开启 SSR 模式。

参考文档:

  • SSR 配置:https://reactrouter.com/explanation/special-files#react-routerconfigts
  • 路由定义:https://reactrouter.com/start/framework/routing

依赖安装

npm i react-router @react-router/node @react-router/serve isbot
npm i -D @react-router/dev vite typescript @types/node @types/react @types/react-dom

框架配置

编辑 react-router.config.ts 开启服务端渲染:

import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
} satisfies Config;

路由定义

编辑 app/routes.ts,支持集中式声明路由、嵌套布局与动态路由参数:

import {
  type RouteConfig,
  index,
  route,
  layout,
} from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  layout("routes/layout.app.tsx", [
    route("dashboard", "routes/dashboard.tsx"),
    route("courses", "routes/courses.tsx"),
    route("courses/:slug", "routes/courses.$slug.tsx"),
    route("admin/categories", "routes/admin.categories.tsx"),
  ]),
  route("login", "routes/login.tsx"),
  route("api/logout", "routes/api.logout.ts"),
] satisfies RouteConfig;

根布局与 HTML 骨架

编辑 app/root.tsx 组织服务端直出结构:

import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "react-router";
import type { Route } from "./+types/root";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}

路由数据加载(Loader)与端到端类型

在服务端 Loader 中直接读取数据库(如 Drizzle ORM),并通过 Typegen 获取强类型推导。

参考文档:

  • 数据加载:https://reactrouter.com/start/framework/data-loading
  • 类型推导:https://reactrouter.com/explanation/typegen

Typegen 配置

在 tsconfig.json 中配置编译路径映射:

{
  "compilerOptions": {
    "rootDirs": [".", "./.react-router/types"]
  }
}

框架会基于路由文件自动生成 ./+types/<route-name>.ts。

服务端 Loader 编写

在路由组件中导出 loader 函数与页面组件:

import type { Route } from "./+types/courses.$slug";
import { db } from "~/db";
import { coursesTable } from "~/db/schema";
import { eq } from "drizzle-orm";
import { data } from "react-router";

export async function loader({ params }: Route.LoaderArgs) {
  const { slug } = params;
  const course = await db.query.coursesTable.findFirst({
    where: eq(coursesTable.slug, slug),
  });

  if (!course) {
    throw data("课程不存在", { status: 404 });
  }

  return { course };
}

export default function CourseDetail({ loaderData }: Route.ComponentProps) {
  const { course } = loaderData;

  return (
    <main>
      <h1>{course.title}</h1>
      <p>{course.description}</p>
    </main>
  );
}

表单突变(Action)与多意图分发

处理客户端提交的数据变更,区分为页面级导航表单与局部独立突变。

参考文档:

  • Action 突变:https://reactrouter.com/start/framework/actions
  • Fetcher 机制:https://reactrouter.com/start/framework/fetchers

表单提交形态

  • <Form>:页面级提交,触发生命周期导航,更新 URL 与浏览器历史记录。
  • useFetcher:局部独立提交,URL 保持不变,支持并发实例与组件级状态隔离。

Action Intent 模式与 Zod 校验

在单一路由中处理多种操作(如创建、更新、删除),通过 intent 字段与 Zod discriminatedUnion 进行强类型分发:

import { useFetcher, data } from "react-router";
import { z } from "zod";
import type { Route } from "./+types/admin.categories";
import { db } from "~/db";
import { categoriesTable } from "~/db/schema";
import { eq } from "drizzle-orm";

const categoryActionSchema = z.discriminatedUnion("intent", [
  z.object({
    intent: z.literal("create"),
    name: z.string().trim().min(1, "分类名称不能为空"),
  }),
  z.object({
    intent: z.literal("update"),
    categoryId: z.coerce.number().int(),
    name: z.string().trim().min(1, "分类名称不能为空"),
  }),
  z.object({
    intent: z.literal("delete"),
    categoryId: z.coerce.number().int(),
  }),
]);

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const submission = Object.fromEntries(formData);
  const parsed = categoryActionSchema.safeParse(submission);

  if (!parsed.success) {
    return data(
      { error: parsed.error.issues[0]?.message ?? "输入数据无效" },
      { status: 400 }
    );
  }

  const payload = parsed.data;

  switch (payload.intent) {
    case "create": {
      await db.insert(categoriesTable).values({ name: payload.name });
      return { success: true, message: "分类创建成功" };
    }
    case "update": {
      await db
        .update(categoriesTable)
        .set({ name: payload.name })
        .where(eq(categoriesTable.id, payload.categoryId));
      return { success: true, message: "分类更新成功" };
    }
    case "delete": {
      await db
        .delete(categoriesTable)
        .where(eq(categoriesTable.id, payload.categoryId));
      return { success: true, message: "分类已删除" };
    }
  }
}

export default function AdminCategories() {
  const createFetcher = useFetcher<typeof action>();
  const deleteFetcher = useFetcher<typeof action>();

  return (
    <div>
      <createFetcher.Form method="post">
        <input type="hidden" name="intent" value="create" />
        <input name="name" placeholder="新建分类名称" />
        <button
          type="submit"
          disabled={createFetcher.state === "submitting"}
        >
          {createFetcher.state === "submitting" ? "提交中..." : "新增"}
        </button>
      </createFetcher.Form>

      {createFetcher.data?.error && (
        <p className="error">{createFetcher.data.error}</p>
      )}
    </div>
  );
}

自动重校验(Revalidation)与性能控制

Action 成功执行后,React Router 默认触发活动 Loader 并行重校验,确保服务端与客户端数据状态一致。

参考文档:

  • Revalidation 控制:https://reactrouter.com/start/framework/revalidation#shouldrevalidate

默认重校验行为

  • 任何 action 执行完毕后,当前页面匹配的所有激活 loader 会被自动重新调用。
  • 数据请求在服务端或客户端均以并行方式执行,避免串行网络瀑布流。

细粒度重校验拦截(shouldRevalidate)

对于高频操作(如实时草稿保存、点赞或只影响局部的数据突变),可通过 shouldRevalidate 拦截不必要的 Loader 调用:

import type { ShouldRevalidateFunctionArgs } from "react-router";

export function shouldRevalidate({
  actionResult,
  defaultShouldRevalidate,
  formData,
}: ShouldRevalidateFunctionArgs) {
  // 校验失败或错误响应时不触发重校验
  if (actionResult?.error) {
    return false;
  }

  // 针对特定 intent 跳过当前路由 loader 的刷新
  const intent = formData?.get("intent");
  if (intent === "track-progress") {
    return false;
  }

  return defaultShouldRevalidate;
}

请求中止与竞态处理

框架原生结合 AbortController。当新的导航或表单提交发生时,会自动中止上一轮未完成的数据请求,防止异步数据竞态覆盖。

会话认证与服务端路由守卫

基于 Web 标准 Cookie 实现服务端会话存储与强类型路由守卫。

参考文档:

  • Session 与 Cookie:https://reactrouter.com/start/framework/cookies

会话存储配置

import { createCookieSessionStorage } from "react-router";

export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "__session",
    httpOnly: true,
    path: "/",
    sameSite: "lax",
    secrets: [process.env.SESSION_SECRET || "default-secret-key"],
    secure: process.env.NODE_ENV === "production",
  },
});

export async function getSession(request: Request) {
  return sessionStorage.getSession(request.headers.get("Cookie"));
}

export async function commitSession(session: any) {
  return sessionStorage.commitSession(session);
}

export async function destroySession(session: any) {
  return sessionStorage.destroySession(session);
}

服务端鉴权守卫

在 loader 或 action 中执行身份校验,未通过时直接抛出 Response 或 data() 阻断渲染:

import type { Route } from "./+types/admin.users";
import { getSession } from "~/lib/session.server";
import { db } from "~/db";
import { usersTable } from "~/db/schema";
import { eq } from "drizzle-orm";
import { data, redirect } from "react-router";

export async function loader({ request }: Route.LoaderArgs) {
  const session = await getSession(request);
  const userId = session.get("userId");

  if (!userId) {
    throw redirect("/login");
  }

  const currentUser = await db.query.usersTable.findFirst({
    where: eq(usersTable.id, userId),
  });

  if (!currentUser || currentUser.role !== "admin") {
    throw data("无权限访问管理后台", { status: 403 });
  }

  const allUsers = await db.select().from(usersTable);
  return { users: allUsers };
}

嵌套错误边界与同构韧性

React Router 提供路由级 ErrorBoundary 机制,在服务端渲染与客户端水合过程中捕获异常并实现局部故障隔离。

参考文档:

  • 错误处理:https://reactrouter.com/start/framework/error-handling

错误捕获与响应区分

使用 isRouteErrorResponse 区分受控 HTTP 错误(如 401、403、404)与未捕获的运行时异常(500):

import { isRouteErrorResponse } from "react-router";
import type { Route } from "./+types/courses.$slug";

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  if (isRouteErrorResponse(error)) {
    return (
      <div className="error-container">
        <h2>{error.status} {error.statusText}</h2>
        <p>{error.data || "请求资源不存在或无访问权限"}</p>
      </div>
    );
  }

  return (
    <div className="error-container">
      <h2>应用运行异常</h2>
      <p>{error instanceof Error ? error.message : "发生未预期的服务故障"}</p>
    </div>
  );
}

局部隔离特性

  • 嵌套路由发生错误时,错误仅被最近一级的 ErrorBoundary 捕获。
  • 上层父级布局(如全局导航栏、侧边栏)保持正常渲染与可交互状态,避免整页白屏崩溃。
目录
初始化与 SSR 配置依赖安装框架配置路由定义根布局与 HTML 骨架路由数据加载(Loader)与端到端类型Typegen 配置服务端 Loader 编写表单突变(Action)与多意图分发表单提交形态Action Intent 模式与 Zod 校验自动重校验(Revalidation)与性能控制默认重校验行为细粒度重校验拦截(shouldRevalidate)请求中止与竞态处理会话认证与服务端路由守卫会话存储配置服务端鉴权守卫嵌套错误边界与同构韧性错误捕获与响应区分局部隔离特性
上一篇Sherpa-ONNX 在 iOS 实现语音合成

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com