中间件
本页内容

中间件



中间件功能目前是实验性的,可能会发生重大变化。请使用 future.unstable_middleware 标志来启用它。

中间件允许你在为匹配路径生成 Response 之前和之后运行代码。这使得像身份验证、日志记录、错误处理和数据预处理等常见模式能够以可重用的方式实现。

中间件以嵌套链的方式运行,在“向下”到你的路由处理程序时,从父路由执行到子路由,然后在生成 Response 后“向上”时,从子路由返回到父路由。

例如,对于一个 GET /parent/child 请求,中间件将按以下顺序运行:

- Root middleware start
  - Parent middleware start
    - Child middleware start
      - Run loaders, generate HTML Response
    - Child middleware end
  - Parent middleware end
- Root middleware end

服务器上的中间件(框架模式)与客户端上的中间件(框架/数据模式)之间存在一些细微差别。在本文档中,我们的大多数示例将引用服务器中间件,因为这对于过去在其他 HTTP 服务器中使用过中间件的用户来说最为熟悉。请参考下面的服务器中间件与客户端中间件部分获取更多信息。

快速入门(框架模式)

1. 启用中间件标志

首先,在你的 React Router 配置中启用中间件:

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

export default {
  future: {
    unstable_middleware: true,
  },
} satisfies Config;

启用中间件功能后,你将更改 loaders 和 actions 的 context 参数类型。如果你目前正在使用 context,请注意下面关于 getLoadContext 的部分。

2. 创建一个上下文

中间件使用 context 提供者实例来向下游的中间件链提供数据。你可以使用 unstable_createContext 创建类型安全的上下文对象:

import { unstable_createContext } from "react-router";
import type { User } from "~/types";

export const userContext =
  unstable_createContext<User | null>(null);

3. 从路由中导出中间件

import { redirect } from "react-router";
import { userContext } from "~/context";

// Server-side Authentication Middleware
async function authMiddleware({ request, context }) {
  const user = await getUserFromSession(request);
  if (!user) {
    throw redirect("/login");
  }
  context.set(userContext, user);
}

export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
  [authMiddleware];

// Client-side timing middleware
async function timingMiddleware({ context }, next) {
  const start = performance.now();
  await next();
  const duration = performance.now() - start;
  console.log(`Navigation took ${duration}ms`);
}

export const unstable_clientMiddleware: Route.unstable_ClientMiddlewareFunction[] =
  [timingMiddleware];

export async function loader({
  context,
}: Route.LoaderArgs) {
  const user = context.get(userContext);
  const profile = await getProfile(user);
  return { profile };
}

export default function Dashboard({
  loaderData,
}: Route.ComponentProps) {
  return (
    <div>
      <h1>Welcome {loaderData.profile.fullName}!</h1>
      <Profile profile={loaderData.profile} />
    </div>
  );
}

4. 更新你的 getLoadContext 函数(如果适用)

如果你正在使用自定义服务器和 getLoadContext 函数,你需要更新你的实现以返回一个 unstable_RouterContextProvider 的实例,而不是一个 JavaScript 对象:

+import {
+  unstable_createContext,
+  unstable_RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";

+const dbContext = unstable_createContext<Database>();

function getLoadContext(req, res) {
-  return { db: createDb() };
+  const context = new unstable_RouterContextProvider();
+  context.set(dbContext, createDb());
+  return context;
}

快速入门(数据模式)

1. 启用中间件标志

const router = createBrowserRouter(routes, {
  future: {
    unstable_middleware: true,
  },
});

2. 创建一个上下文

中间件使用 context 提供者实例来向下游的中间件链提供数据。你可以使用 unstable_createContext 创建类型安全的上下文对象:

import { unstable_createContext } from "react-router";
import type { User } from "~/types";

export const userContext =
  unstable_createContext<User | null>(null);

3. 将中间件添加到路由中

import { redirect } from "react-router";
import { userContext } from "~/context";

const routes = [
  {
    path: "/",
    unstable_middleware: [timingMiddleware], // 👈
    Component: Root,
    children: [
      {
        path: "profile",
        unstable_middleware: [authMiddleware], // 👈
        loader: profileLoader,
        Component: Profile,
      },
      {
        path: "login",
        Component: Login,
      },
    ],
  },
];

async function timingMiddleware({ context }, next) {
  const start = performance.now();
  await next();
  const duration = performance.now() - start;
  console.log(`Navigation took ${duration}ms`);
}

async function authMiddleware({ context }) {
  const user = await getUser();
  if (!user) {
    throw redirect("/login");
  }
  context.set(userContext, user);
}

export async function profileLoader({
  context,
}: Route.LoaderArgs) {
  const user = context.get(userContext);
  const profile = await getProfile(user);
  return { profile };
}

export default function Profile() {
  let loaderData = useLoaderData();
  return (
    <div>
      <h1>Welcome {loaderData.profile.fullName}!</h1>
      <Profile profile={loaderData.profile} />
    </div>
  );
}

4. 添加一个 unstable_getContext() 函数(可选)

如果你希望在所有导航/获取操作中包含一个基础上下文,可以向你的路由器添加一个 unstable_getContext 函数。每次导航/获取时都会调用此函数来填充一个新的上下文。

let sessionContext = unstable_createContext();

const router = createBrowserRouter(routes, {
  future: {
    unstable_middleware: true,
  },
  unstable_getContext() {
    let context = new unstable_RouterContextProvider();
    context.set(sessionContext, getSession());
    return context;
  },
});

此 API 旨在模仿服务器端框架模式下的 getLoadContext API,该 API 用于将值从你的 HTTP 服务器传递给 React Router 处理程序。这个 unstable_getContext API 可用于将 window/document 中的全局值传递给 React Router,但由于它们都在同一个上下文(浏览器)中运行,你实际上可以通过根路由中间件实现相同的行为。因此,你可能不像在服务器上那样需要这个 API——但为了保持一致性,我们提供了它。

核心概念

服务器中间件 vs 客户端中间件

服务器中间件在框架模式下的服务器上运行,用于处理 HTML 文档请求和后续导航及 fetcher 调用的 .data 请求。因为服务器中间件是在服务器上响应 HTTP Request 运行的,所以它通过 next 函数将 HTTP Response 返回到中间件链的上层:

async function serverMiddleware({ request }, next) {
  console.log(request.method, request.url);
  let response = await next();
  console.log(response.status, request.method, request.url);
  return response;
}

// Framework mode only
export const unstable_middleware = [serverMiddleware];

客户端中间件在浏览器中的框架模式和数据模式下运行,用于客户端导航和 fetcher 调用。客户端中间件的不同之处在于没有 HTTP 请求,因此它不会通过 next 函数冒泡任何东西:

async function clientMiddleware({ request }, next) {
  console.log(request.method, request.url);
  await next(); // 👈 No return value
  console.log(response.status, request.method, request.url);
  // 👈 No need to return anything here
}

// Framework mode
export const unstable_clientMiddleware = [clientMiddleware];

// Or, Data mode
const route = {
  path: "/",
  unstable_middleware: [clientMiddleware],
  loader: rootLoader,
  Component: Root,
};

中间件何时运行

了解你的中间件何时运行非常重要,以确保你的应用程序按预期运行。

服务器中间件

在一个已注水的框架模式应用中,服务器中间件的设计优先考虑 SPA 行为,默认情况下不会创建新的网络活动。中间件包装现有请求,并且仅在您需要访问服务器时运行。

这就提出了一个问题:在 React Router 中,“处理程序”是什么?是路由吗?还是 loader?我们认为“视情况而定”。

  • 在文档请求(GET /route)中,处理程序是路由——因为响应同时包含了 loader 和路由组件。
  • 在用于客户端导航的数据请求(GET /route.data)中,处理程序是 loader/action,因为响应中只包含这些内容。

因此:

  • 文档请求会运行服务器中间件,无论是否存在 loaders,因为我们仍然在渲染 UI 的“处理程序”中。
  • 客户端导航仅在向服务器发出 .data 请求以获取 loader/action 时才会运行服务器中间件。

对于请求注释中间件,如记录请求时长、检查/设置会话、设置传出缓存头等,这种行为非常重要。如果本来就没有理由访问服务器,那么去服务器运行这些类型的中间件将是无用的。这会导致服务器负载增加和服务器日志嘈杂。

// This middleware won't run on client-side navigations without a `.data` request
function loggingMiddleware({ request }, next) {
  console.log(`Request: ${request.method} ${request.url}`);
  let response = await next();
  console.log(
    `Response: ${response.status} ${request.method} ${request.url}`,
  );
  return response;
}

export const unstable_middleware = [loggingMiddleware];

然而,在某些情况下,你可能希望每次客户端导航时都运行某些服务器中间件——即使没有 loader 存在。例如,在你的网站的已认证部分有一个表单,它不需要 loader,但你宁愿使用身份验证中间件在用户填写表单之前就将他们重定向——而不是在他们提交到 action 时。如果你的中间件符合这个标准,那么你可以在包含该中间件的路由上放置一个 loader,以强制它在涉及该路由的客户端导航中总是调用服务器。

function authMiddleware({ request }, next) {
  if (!isLoggedIn(request)) {
    throw redirect("/login");
  }
}

export const unstable_middleware = [authMiddleware];

// By adding a loader, we force the authMiddleware to run on every client-side
// navigation involving this route.
export function loader() {
  return null;
}

客户端中间件

客户端中间件更简单,因为我们已经在客户端,并且在导航时总是向路由器发出“请求”。客户端中间件将在每次客户端导航时运行,无论是否有要运行的 loaders。

Context API

新的上下文系统提供了类型安全,并防止了命名冲突,允许你向嵌套的中间件和 loader/action 函数提供数据。在框架模式下,这取代了之前的 AppLoadContext API。

// ✅ Type-safe
import { unstable_createContext } from "react-router";
const userContext = unstable_createContext<User>();

// Later in middleware/loaders
context.set(userContext, user); // Must be User type
const user = context.get(userContext); // Returns User type

// ❌ Old way (no type safety)
// context.user = user; // Could be anything

上下文和 AsyncLocalStorage

Node 提供了一个 AsyncLocalStorage API,它提供了一种通过异步执行上下文传递值的方式。虽然这是一个 Node API,但大多数现代运行时都已经(大部分)使其可用(例如,CloudflareBunDeno)。

理论上,我们本可以直接利用 AsyncLocalStorage 作为从中间件向子路由传递值的方式,但缺乏 100% 的跨平台兼容性令人担忧,因此我们仍然希望提供一个一流的 context API,以便有一种发布可重用的中间件包的方式,保证以运行时无关的方式工作。

也就是说,这个 API 仍然可以很好地与 React Router 中间件配合使用,并且可以替代 context API 或与之并用。

当使用React 服务器组件时,AsyncLocalStorage 尤其强大,因为它允许你从 middleware 向你的服务器组件和服务器操作提供信息,因为它们在同一个服务器执行上下文中运行 🤯

import { AsyncLocalStorage } from "node:async_hooks";

const USER = new AsyncLocalStorage<User>();

export async function provideUser(
  request: Request,
  cb: () => Promise<Response>,
) {
  let user = await getUser(request);
  return USER.run(user, cb);
}

export function getUser() {
  return USER.getStore();
}
import { provideUser } from "./user-context";

export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
  [
    async ({ request, context }, next) => {
      return provideUser(request, async () => {
        let res = await next();
        return res;
      });
    },
  ];
import { getUser } from "../user-context";

export function loader() {
  let user = getUser();
  //...
}

next 函数

next 函数的逻辑取决于它从哪个路由中间件中被调用:

  • 当从非叶子中间件调用时,它运行链中的下一个中间件。
  • 当从叶子中间件调用时,它执行任何路由处理程序并为请求生成最终的 Response
const middleware = async ({ context }, next) => {
  // Code here runs BEFORE handlers
  console.log("Before");

  const response = await next();

  // Code here runs AFTER handlers
  console.log("After");

  return response; // Optional on client, required on server
};

每个中间件只能调用一次 next()。多次调用会抛出错误。

跳过 next()

如果你不需要在处理程序之后运行代码,可以跳过调用 next()

const authMiddleware = async ({ request, context }) => {
  const user = await getUser(request);
  if (!user) {
    throw redirect("/login");
  }
  context.set(userContext, user);
  // next() is called automatically
};

next() 和错误处理

React Router 通过路由的 ErrorBoundary 导出提供了内置的错误处理。就像 loader/action 抛出错误时一样(基本上),如果一个中间件抛出错误,它将被相应的 ErrorBoundary 捕获和处理,并且一个 Response 将通过祖先的 next() 调用返回。这意味着 next() 函数永远不应该抛出错误,并且应该总是返回一个 Response,所以你不需要担心将它包装在 try/catch 中。

这种行为对于允许中间件模式非常重要,例如从根中间件自动为传出的响应设置必需的标头(例如,提交一个会话)。如果来自中间件的任何错误导致 next() throw,我们就会错过在返回时执行祖先中间件,而那些必需的标头就不会被设置。

// routes/parent.tsx
export const unstable_middleware = [
  async (_, next) => {
    let res = await next();
    //  ^ res.status = 500
    // This response contains the ErrorBoundary
    return res;
  }
]

// routes/parent.child.tsx
export const unstable_middleware = [
  async (_, next) => {
    let res = await next();
    //  ^ res.status = 200
    // This response contains the successful UI render
    throw new Error('Uh oh, something went wrong!)
  }
]

我们在上面说“基本上”,是因为在 loader/action 中抛出非重定向的 Response 与在 middleware 中抛出 Response(重定向行为相同)之间有一个微小而微妙的区别。

从中间件抛出非重定向响应将直接使用该响应,并通过父级的 next() 调用返回它。这与 loaders/actions 中的行为不同,在后者中,该响应将被转换为一个 ErrorResponse,由 ErrorBoundary 渲染。

这里的区别在于,loaders/actions 期望返回数据,然后提供给组件进行渲染。但中间件期望返回一个响应——所以如果你返回或抛出一个响应,我们将直接使用它。如果你想从中间件向错误边界抛出一个带有状态码的错误,你应该使用 data 工具。

getLoadContext/AppLoadContext 的更改

这仅适用于您正在使用自定义服务器和自定义 getLoadContext 函数的情况。

中间件对由 getLoadContext 生成并传递给你的 loaders 和 actions 的 context 参数引入了一个破坏性变更。当前通过模块增强 AppLoadContext 的方法并不是真正的类型安全,而只是告诉 TypeScript“相信我”。

中间件在客户端需要一个等效的 context 用于 clientMiddleware,但我们不想复制服务器上我们已经不太满意的这种模式,所以我们决定引入一个新的 API,以便我们可以解决类型安全问题。

选择使用中间件时,context 参数会变为 RouterContextProvider 的一个实例。

let dbContext = unstable_createContext<Database>();
let context = new unstable_RouterContextProvider();
context.set(dbContext, getDb());
//                     ^ type-safe
let db = context.get(dbContext);
//  ^ Database

如果你正在使用自定义服务器和 getLoadContext 函数,你需要更新你的实现,以返回一个 unstable_RouterContextProvider 的实例,而不是一个普通的 JavaScript 对象。

+import {
+  unstable_createContext,
+  unstable_RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";

+const dbContext = unstable_createContext<Database>();

function getLoadContext(req, res) {
-  return { db: createDb() };
+  const context = new unstable_RouterContextProvider();
+  context.set(dbContext, createDb());
+  return context;
}

AppLoadContext 迁移

如果你当前正在使用 AppLoadContext,你可以通过使用现有的模块增强来增强 unstable_RouterContextProvider 而不是 AppLoadContext,从而进行增量迁移。然后,更新你的 getLoadContext 函数以返回一个 unstable_RouterContextProvider 的实例。

declare module "react-router" {
-  interface AppLoadContext {
+  interface unstable_RouterContextProvider {
    db: Database;
    user: User;
  }
}

function getLoadContext() {
  const loadContext = {...};
-  return loadContext;
+  let context = new unstable_RouterContextProvider();
+  Object.assign(context, loadContext);
+  return context;
}

这允许你在初次采用中间件时保持你的 loaders/actions 不变,因为它们仍然可以直接读取值(即 context.db)。

这种方法仅旨在作为在 React Router v7 中采用中间件时的迁移策略,允许你逐步迁移到 context.set/context.get。不能保证这种方法在 React Router 的下一个主要版本中仍然有效。

unstable_RouterContextProvider 类也用于客户端的 context 参数,通过 <HydratedRouter unstable_getContext><RouterProvider unstable_getContext>。由于 AppLoadContext 主要用作从你的 HTTP 服务器到 React Router 处理程序的传递,你需要注意,即使 TypeScript 会告诉你这些增强字段可用,它们在 clientMiddlewareclientLoaderclientAction 函数中将不可用(除非,当然,你通过客户端的 unstable_getContext 提供了这些字段)。

常见模式

身份验证

import { redirect } from "react-router";
import { userContext } from "~/context";
import { getSession } from "~/sessions.server";

export const authMiddleware = async ({
  request,
  context,
}) => {
  const session = await getSession(request);
  const userId = session.get("userId");

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

  const user = await getUserById(userId);
  context.set(userContext, user);
};
import { authMiddleware } from "~/middleware/auth";

export const unstable_middleware = [authMiddleware];

export function loader({ context }: Route.LoaderArgs) {
  const user = context.get(userContext); // Guaranteed to exist
  return { user };
}

日志记录

import { requestIdContext } from "~/context";

export const loggingMiddleware = async (
  { request, context },
  next,
) => {
  const requestId = crypto.randomUUID();
  context.set(requestIdContext, requestId);

  console.log(
    `[${requestId}] ${request.method} ${request.url}`,
  );

  const start = performance.now();
  const response = await next();
  const duration = performance.now() - start;

  console.log(
    `[${requestId}] Response ${response.status} (${duration}ms)`,
  );

  return response;
};

404 到 CMS 重定向

export const cmsFallbackMiddleware = async (
  { request },
  next,
) => {
  const response = await next();

  // Check if we got a 404
  if (response.status === 404) {
    // Check CMS for a redirect
    const cmsRedirect = await checkCMSRedirects(
      request.url,
    );
    if (cmsRedirect) {
      throw redirect(cmsRedirect, 302);
    }
  }

  return response;
};

响应头

export const headersMiddleware = async (
  { context },
  next,
) => {
  const response = await next();

  // Add security headers
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");

  return response;
};

条件中间件

export const unstable_middleware = [
  async ({ request, context }, next) => {
    // Only run auth for POST requests
    if (request.method === "POST") {
      await ensureAuthenticated(request, context);
    }
    return next();
  },
];

在 Action 和 Loader 之间共享上下文

const sharedDataContext = unstable_createContext<any>();

export const unstable_middleware = [
  async ({ request, context }, next) => {
    if (request.method === "POST") {
      // Set data during action phase
      context.set(
        sharedDataContext,
        await getExpensiveData(),
      );
    }
    return next();
  },
];

export async function action({
  context,
}: Route.ActionArgs) {
  const data = context.get(sharedDataContext);
  // Use the data...
}

export async function loader({
  context,
}: Route.LoaderArgs) {
  const data = context.get(sharedDataContext);
  // Same data is available here
}
文档和示例 CC 4.0
编辑