Next 16 的 proxy.ts 固定跑在 Node;本站仍用 middleware.ts 做边缘改头演示,页面能读到同一枚请求头。matcher 之外的路由不会经过它
headers() 看到的是请求,不是 curl -I 里的响应。两边都写了同一个名字
x-playground-middleware: demo
读 headers() 会让这一页变成动态渲染。这是代价的一部分:要在渲染时看见请求级信息,就不能再把整页冻成静态 HTML。
包根 middleware.ts;matcher 只命中本页路径
// middleware.ts —— 包根,与 app/ 同级
import { NextResponse, type NextRequest } from "next/server";
import { DEMO_HEADER } from "@/lib/demo-header";
export function middleware(request: NextRequest) {
// 请求头:页面里 headers() 读得到
const requestHeaders = new Headers(request.headers);
requestHeaders.set(DEMO_HEADER, "demo");
const response = NextResponse.next({
request: { headers: requestHeaders },
});
// 响应头:curl -I 看得到。两个头不是同一个对象
response.headers.set(DEMO_HEADER, "demo");
return response;
}
// matcher:只有命中的路径才执行 middleware,其余路由零开销
export const config = {
matcher: ["/engineering/middleware"],
};# 生产构建后验证(dev 下同样生效): curl -I http://localhost:3000/engineering/middleware # HTTP/2 200 # x-playground-middleware: demo ← 自定义头在这里 curl -I http://localhost:3000/ # HTTP/2 200 # (没有 x-playground-middleware) ← 未命中 matcher,中间件未执行
源码见packages/next-playground/middleware.ts。页面上的字来自请求头;curl -I 看到的是响应头。运行时差异见下方「proxy.ts 与 middleware.ts」。
在路由匹配、缓存查找、渲染之前执行;部署到边缘时离用户最近
"/((?!_next/static|_next/image|favicon.ico).*)"排除静态资产,只拦页面与 API四种职责都建立在「请求还没进渲染管线」这个事实之上
| 能力 | API | 典型场景 |
|---|---|---|
| 改写 rewrite | NextResponse.rewrite() | URL 不变、内容换源(多租户路径映射、A/B 实验分桶) |
| 重定向 redirect | NextResponse.redirect() | 未登录跳 /login、地区跳转、旧链接迁移 |
| 头部 header | NextResponse.next() 后改 headers | 本站演示;给下游传请求级上下文(实验分组、追踪 ID) |
| 鉴权门 auth gate | 读 cookie / header 粗判后放行或拦截 | 「有没有票」的门口检查,见下方代码 |
// ✅ 中间件做「门」:有无、过期的粗判,拦住明显未登录的请求
export function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next(); // 放行,细粒度判断交给页面/接口
}
// ❌ 中间件别做「账」:权限矩阵、配额、行级数据归属这类判断
// 需要查库/读业务状态,放中间件里既慢又受运行时限制;
// 正确的位置是页面/Route Handler 内部,或受保护的数据访问层改名同时改了默认运行时。本站留着旧文件,是因为这则演示要跑在边缘上
决策要点:每个匹配请求都要付一次执行成本,且运行时是受限的