Remix 3 自定义中间件怎么写:用 context.set 提供类型化请求值
2026/9/10 5:29:43 网站建设 项目流程

Remix 3 自定义中间件怎么写:用 context.set 提供类型化请求值

【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix

在 Remix 3 的应用里,你经常会遇到这类需求:每个请求都要带上一个请求 ID、一个数据库连接、或一个解析好的用户信息,并且下游的 controller、action 希望以带类型的方式读取这些值。Remix 3 的路由器通过(context, next)中间件契约解决这个问题:中间件用context.set(key, value)写入请求级值,下游代码用context.get(key)读取,并且 TypeScript 能根据中间件的类型推导出读取值的准确类型。本文基于 Request Handling 章节 的 "Custom middleware" 与 "Typed request context" 部分,配合仓库中的真实示例,走一遍从定义中间件到在 action 中类型安全地读取值的完整路径。

先明确中间件能拿到什么、能做什么

Remix 3 的每个中间件接收(context, next),返回一个Response,或者返回next()的结果。同一次router.fetch(...)调用里,所有中间件和 action 拿到的是同一个请求上下文对象,它初始带有原始request、解析后的url、可变的Headers副本、生效的method、匹配到的params和当前router。这些内置字段在 Routing and Controllers 章节 的表格中有完整列出,其中与本文直接相关的三个方法是:

context.set(key, value) // 在共享的请求上下文上存储一个请求级值 context.get(key) // 读取某个 context key 存储的值 context.has(key) // 判断某个 context key 是否已有值

两条执行规则决定你写中间件时的位置:

  • await next()之前的代码在请求进入时执行,之后的代码在响应回程执行,可以检查或替换下游返回的响应;
  • 不调用next()直接返回Response会中断链条——静态文件、CORS 预检、鉴权拒绝和缓存都靠这个机制提前应答。

Remix 有三种中间件作用域:router 中间件在路由匹配前对每个请求运行;controller 中间件运行于该 controller 直接拥有的 action;action 中间件只运行于单个 action。控制器和 action 中间件只在路由匹配后才执行。

第一步:用 createContextKey 定义一个类型化 key

不要直接用字符串当 key。remix/router导出的createContextKey创建一个类型安全的 key,它的泛型参数就是存储值的类型:

import { createContextKey, type Middleware } from "remix/router"; export const RequestId = createContextKey<string>();

createContextKey也可以带一个默认值:createContextKey<value>(defaultValue)。有默认值时,context.get(key)在未 set 的情况下返回默认值;没有默认值时返回undefined。这一点在 RequestContext 源码 的get实现中可以直接核对。

第二步:写中间件,并用 Middleware 类型声明它提供的值

文档给出的示例是一个请求 ID 中间件:在 action 执行前写入 ID,在响应回程把同一个 ID 加进响应头:

import { createContextKey, type Middleware } from "remix/router"; export const RequestId = createContextKey<string>(); export function requestId(): Middleware<{ key: typeof RequestId; value: string; }> { return async (context, next) => { let id = crypto.randomUUID(); context.set(RequestId, id); let response = await next(); let headers = new Headers(response.headers); headers.set("X-Request-Id", id); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); }; }

Middleware<{ key, value }>这个类型参数是关键:它向 Remix 的类型系统声明"这个中间件往上下文里加了什么"。一旦requestId()出现在有类型的 router 中间件栈里,下游 controller 里context.get(RequestId)的类型就是string,而不是string | undefined

如果中间件只是拒绝请求而不提供值,就不需要类型参数,直接返回Response且不调用next()

function requireJson(): Middleware { return (context, next) => { let mediaType = context.headers.get("Content-Type")?.split(";", 1)[0].trim().toLowerCase(); if (mediaType !== "application/json") { return new Response("Expected JSON", { status: 415 }); } return next(); }; }

第三步:把中间件放进 router,让类型流入 controller

app/router.ts中,把requestId()加进createRoutermiddleware数组,然后用RouterContext<typeof router>从当前 router 推导出完整的应用上下文,再通过模块扩展把它设为 controller 的默认上下文:

import { render } from "remix/middleware/render"; import { staticFiles } from "remix/middleware/static"; import { createRouter, type RouterContext } from "remix/router"; import { requestId } from "./middleware/request-id.ts"; import controller from "./actions/controller.tsx"; import { routes } from "./routes.ts"; export const router = createRouter({ middleware: [staticFiles("./public", { index: false }), requestId(), render()], }); export type AppContext = RouterContext<typeof router>; declare module "remix/router" { interface RouterTypes { context: AppContext; } } router.map(routes, controller);

RouterContext<typeof router>按中间件数组的顺序逐项收集每个中间件声明的 context 条目,所以放在栈里的顺序必须满足依赖关系:提供者要放在消费者前面(文档给的例子是formData()methodOverride()之前、compression()在它要包裹的staticFiles()之前)。

模块扩展之所以有用,是因为 controller 在独立文件里创建、之后才被router.map(...)映射到这个 router,它们不可能显式引用这个 router 的类型。文档同时提醒:一个应用里有多个 router 时,应改为给每个 controller 传显式的 context 类型,而不是设一个全局默认。

到此,controller 里的 action 就能直接以string类型读取请求 ID:

// inside an action: action(context) { let id = context.get(RequestId); // 类型是 string // ... }

可选:把值安装为 context 的直接属性

context.set(key, value)接受第三个参数{ property },用于把值安装成请求上下文上的一个只读直接属性。仓库里的真实例子是 bookstore 演示的数据库中间件:

import { createContextKey, type Middleware } from 'remix/router' export const databaseContext = createContextKey<Database>() export function loadDatabase(): Middleware<{ key: typeof databaseContext value: Database property: 'db' }> { return (context, next) => { context.set(databaseContext, db, { property: 'db' }) return next() } }

安装后下游可以像context.db一样直接访问,而不是每次context.get(databaseContext)。内置的formData()提供context.formDatarender()提供context.render(...)走的就是同一机制。

从 RequestContext 实现 可以看到这个机制的三条约束,违反时会在运行时抛错:

  • 属性名不能与RequestContext上已有的属性(如requesturlheaders)重名;
  • 同一个 context key 不能先后用两个不同的属性名;
  • 不同的 context key 不能安装到同一个属性名。

验证:类型层面和响应头两个检查点

类型检查。判断中间件是否正确接入的最直接方式是编译:requestId()进入createRoutermiddleware数组后,context.get(RequestId)推导为string;如果中间件不在栈里,同一次读取推导为string | undefined(key 无默认值时运行结果是undefined)。用tsc或你 IDE 的类型提示确认这一点,不需要运行服务器。

响应头检查。按上面的requestId()示例,每个经过该中间件的响应都会带上X-Request-Id请求头。启动server.ts(默认端口见 Request Handling 章节 的 Node 入口示例,默认 44100)后,对任意路由发起请求,检查响应头中是否存在X-Request-Id。它由crypto.randomUUID()生成,每次请求值不同,文档没有给出固定预期值,检查时应以"该请求的响应头里存在且与后续日志一致"为判断依据。

边界与替代路径

  • 中间件链存变量时。内联middleware: [...]数组是默认写法。只有当一条可复用的中间件链必须存进变量时,才用createMiddleware(...)保留它的 tuple 类型,并用MiddlewareContext<typeof middleware>推导结果上下文。
  • action 之外的辅助代码要读值时。请求上下文是显式的 per-request 值,不是全局。中间件和 action 之外的 helper 要拿它,从remix/middleware/async-context引入asyncContext()加进中间件栈,然后在 helper 里调getContext();该中间件依赖 Node 的 async context 支持,跨运行时可移植的做法是把需要的值作为函数参数传进去。
  • 职责边界。文档建议自定义中间件聚焦单一请求生命周期关注点:路由特定的数据加载或校验放 action;跨多路由共享的行为——请求 ID、session、认证、安全头、数据库访问——才放进中间件管线。
  • 运行时可移植性。router 契约本身可移植,但中间件栈里每个包未必:例如静态文件和压缩中间件用了 Node 的 API,在 worker 上应改用平台能力。

写完后对照两条文档给出的选型标准自查一遍:这个值是否真正请求级(换掉它应该随请求变化)?下游读取点是否需要类型保证而不是每次判空?两者都成立时,createContextKey+context.set+Middleware<{ key, value }>就是完整方案。

【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询