中间件
middleware.ts:通过 config.matcher,在每次请求前执行重定向、重写及请求头和 Cookie 处理。
中间件在请求完成前运行代码。它可以重定向、重写、设置标头和 Cookie,或直接返回响应。
约定
在项目根目录(或 src/)中创建 middleware.ts,与 app/ 同级。导出一个 middleware 函数(或默认导出)和可选的 config。
import { NextResponse, type NextRequest } from "@textjs/app/server";export function middleware(request: NextRequest) { if (!request.cookies.has("session")) { return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next();}export const config = { matcher: ["/dashboard/:path*", "/api/:path*"],};在 text dev、text start 和每个服务器部署适配器上,中间件都先于路由处理程序、静态文件和控制台页面运行。它使用 Web Request 和 Response,因此同一个文件可在 Node、Bun、Vercel Edge 和 Cloudflare Workers 上运行。
路径匹配
未设置 config.matcher 时,中间件会为每个请求运行。匹配器使用与 Next.js 相同的语法:
| 匹配器 | 匹配内容 |
|---|---|
/about | /about 和 /about/ |
/blog/:slug | /blog/hello |
/docs/:path* | /docs、/docs/a/b |
/a/:rest+ | /a/b,不匹配 /a |
/user/:id(\\d+) | /user/42,不匹配 /user/ada |
/((?!api/).*) | 除 /api/* 之外的所有路径 |
使用 has 和 missing 添加条件(标头、Cookie、查询或主机):
export const config = { matcher: [{ source: "/beta/:path*", has: [{ type: "cookie", key: "beta", value: "1" }] }],};匹配器应用于不含 basePath 的路径名。在 text dev 中,Vite 自身的模块请求(/@vite/*、源文件)从不运行中间件。
每个宿主都会在中间件运行前确定一个规范路径名,用于匹配、request.nextUrl 以及路由、处理程序和文件查找。百分号转义会先解码,再按 new URL() 的写入方式重新编码路径,同时解析 . 和 .. 路径段,并移除重复斜杠和末尾斜杠。/ad%6din//users/ 到达中间件时为 /admin/users,而 /café 则保持为 /caf%C3%A9。带有末尾斜杠的匹配器(如 /admin/)会匹配 /admin。路径中包含编码后的斜杠或反斜杠(%2F、%5C)、控制字符或无效百分号编码的请求会收到 400。路由区分大小写:/Admin 不匹配 /admin,静态文件也只通过完全一致的名称提供。
匹配文件实际提供服务的 URL,不要只匹配文件名。Cloudflare Pages 也会在 /report 提供 report.html,因此 /report.html 的匹配器无法保护 /report。同时列出两者,即 matcher: ["/report", "/report.html"],或将受保护文件放在一个目录中并匹配该目录,例如用 /reports/:path* 匹配 public/reports/ 中的文件。
NextResponse
| API | 效果 |
|---|---|
NextResponse.next() | 继续执行路由 |
NextResponse.redirect(url, status?) | 默认为 307;url 必须是绝对 URL |
NextResponse.rewrite(url) | 提供另一个路径的内容;对外部源使用代理 |
NextResponse.json(body, init?) | 返回 JSON 响应 |
new Response(...) | 返回任意内容的响应 |
不返回任何内容即可保持原样继续执行。next() 和 rewrite() 接受 { status },用于更改最终响应的状态。只有通过 NextResponse.next() / NextResponse.rewrite() 创建的响应才会影响路由:其他任何 Response,包括 fetch() 返回的响应,都会移除 x-middleware-* 标头后原样发送给客户端。
request.nextUrl 的 pathname 不包含 basePath,而 href 和 toString() 包含它。要确保仍在应用内,请克隆该 URL:const url = request.nextUrl.clone(); url.pathname = "/login"; return NextResponse.redirect(url);。
export function middleware(request: NextRequest) { if (request.nextUrl.pathname === "/status") { return NextResponse.rewrite(new URL("/api/health", request.url)); }}标头
传入 request.headers 可更改下游处理程序收到的标头,传入 headers 可添加响应标头:
export function middleware(request: NextRequest) { const headers = new Headers(request.headers); headers.set("x-user", "ada"); const response = NextResponse.next({ request: { headers } }); response.headers.set("x-frame", "▼"); return response;}中间件的响应标头会添加到最终响应中,除非路由处理程序设置了同名标头。
Cookie
request.cookies 读取 Cookie 标头;response.cookies 写入 Set-Cookie。
export function middleware(request: NextRequest) { const theme = request.cookies.get("theme")?.value ?? "geistDark"; const response = NextResponse.next(); response.cookies.set("theme", theme, { httpOnly: true, sameSite: "lax", maxAge: 60 * 60 * 24 }); return response;}在 next() 或 rewrite() 响应上设置的 Cookie,对同一请求中的路由处理程序也可见。
后台任务
第二个参数提供 waitUntil(),可在响应发送后保持无服务器运行时存活:
import type { NextFetchEvent, NextRequest } from "@textjs/app/server";export function middleware(request: NextRequest, event: NextFetchEvent) { event.waitUntil(fetch("https://analytics.example.com", { method: "POST", body: request.url }));}说明
- 客户端渲染的页面在浏览器中渲染,因此重写到另一个页面会更改提供的文件,不会更改控制台应用渲染的路由。重写到路由处理程序、静态文件和外部源的行为符合预期。
- 中间件可以读取请求体;路由处理程序仍会收到请求体。匹配器排除的请求,其请求体始终不会被读取或修改。
request.url始终使用服务器的源://other.host/x这样的请求目标会规范化为只有一个前导斜杠的路径。- 静态适配器无法运行中间件。