Pages Functions 中间件
上一篇把 Pages Functions 的端点写法和动态路由讲完了。随着接口变多,你会发现很多逻辑在每个端点里重复,比如校验登录、记日志、处理异常。把这些公共逻辑抽出来统一执行,就是中间件要做的事。这一篇讲 _middleware 文件的写法和作用域,再讲 _routes.json 怎么控制哪些请求走函数,最后用 A/B 测试把中间件用起来。
中间件是什么
中间件是在端点函数之前执行的一段逻辑。它能拿到请求,做些预处理,然后决定是继续交给后面的端点,还是直接返回。常见用途如下。
| 用途 | 说明 |
|---|---|
| 鉴权 | 校验请求头里的 Token,不通过直接返回 401 |
| 日志 | 记录请求方法、路径、耗时 |
| 异常处理 | 捕获端点抛出的错误,统一返回 500 |
| A/B 测试 | 按比例分流,给不同用户返回不同版本 |
| 请求改写 | 修改请求头或路径再往下传 |
中间件和端点的区别在于,端点处理完就返回,中间件通常调用 context.next() 把控制权交给下一层。
_middleware 文件
中间件用 _middleware.js 文件定义,文件名是固定的。它放在哪个目录,就对哪个目录及其子目录的所有端点生效。
假设有如下目录结构。
functions/
├── _middleware.js
├── api/
│ ├── _middleware.js
│ ├── users.js
│ └── posts.js
└── index.js
两个中间件的作用域如下。
| 文件路径 | 作用范围 |
|---|---|
| functions/_middleware.js | 整个项目所有端点,包括静态资源请求 |
| functions/api/_middleware.js | /api 下的所有端点,如 /api/users、/api/posts |
最外层的 functions/_middleware.js 作用域最广,连静态文件请求也会经过它。只想给接口加中间件,就放到 functions/api/_middleware.js,别放最外层。
中间件的基本写法和端点一样,导出 onRequest,区别是要调用 context.next()。
// functions/api/_middleware.js
export async function onRequest(context) {
const token = context.request.headers.get("Authorization");
if (!token) {
return new Response("未授权", { status: 401 });
}
// 校验通过,继续执行后续端点
return await context.next();
}
context.next() 返回的是后续端点的响应,你可以直接返回它,也可以在它基础上改。
异常处理中间件
中间件最常见的用法之一是统一异常处理。把 context.next() 包在 try/catch 里,任何端点抛错都能被接住。
// functions/_middleware.js
export async function onRequest(context) {
try {
return await context.next();
} catch (err) {
return new Response(
JSON.stringify({ error: err.message }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
这样每个端点就不用自己写 try/catch,出错时返回格式统一,前端处理也方便。
中间件的执行顺序
多层中间件同时存在时,执行顺序是从外到内。外层目录的中间件先跑,内层目录的中间件后跑,最后才是端点。
| 执行顺序 | 来源 |
|---|---|
| 1 | functions/_middleware.js |
| 2 | functions/api/_middleware.js |
| 3 | 端点函数 |
每个中间件调用 context.next() 后会暂停,等内层全部执行完,控制权再一层层返回。所以可以在 next() 之前做请求预处理,在 next() 之后做响应后处理。
export async function onRequest(context) {
const start = Date.now();
// 请求阶段,记录开始时间
const response = await context.next();
// 响应阶段,计算耗时
const cost = Date.now() - start;
response.headers.set("X-Response-Time", cost + "ms");
return response;
}
一个文件里还能导出多个中间件,用数组形式串联。
// functions/api/_middleware.js
async function errorHandling(context) {
try {
return await context.next();
} catch (err) {
return new Response("服务器错误", { status: 500 });
}
}
async function auth(context) {
if (!context.request.headers.get("Authorization")) {
return new Response("未授权", { status: 401 });
}
return context.next();
}
export const onRequest = [errorHandling, auth];
数组里中间件按顺序执行,errorHandling 包在最外层能捕获 auth 抛出的错误。
_routes.json 控制路由
加了 Functions 之后有一个重要的成本问题。默认情况下,项目的所有请求都会触发 Functions 执行,包括那些只是取静态文件的请求。静态请求原本免费不限量,一旦全部走函数,就按函数调用次数计费了。
_routes.json 用来解决这个问题,它声明哪些路径走函数、哪些路径直接返回静态文件。文件放在项目输出目录根下。
{
"version": 1,
"include": ["/api/*"],
"exclude": ["/api/static/*"]
}
三个字段的含义如下。
| 字段 | 作用 |
|---|---|
| version | 固定为 1 |
| include | 匹配这些路径的请求才走函数 |
| exclude | 从 include 里剔除这些路径,直接返回静态文件 |
include 和 exclude 的组合效果如下。
| 写法 | 效果 |
|---|---|
| include: [“/api/*”] | 只有 /api 下的请求走函数,其余全静态 |
| include: [“/*”] | 所有请求都走函数 |
| include: [“/“], exclude: [“/assets/“] | 除了 /assets 走静态,其余都走函数 |
原则是 include 尽量收窄,只把真正需要动态处理的路径放进去。/assets/*、/static/* 这类纯静态资源目录一定要 exclude,避免无谓的函数调用。
如果项目用 Pages CI 或 Wrangler 部署且检测到 functions 目录,Pages 会自动生成一个 _routes.json,但自动生成的规则通常比较宽,建议手动检查并收紧。
A/B 测试实践
把前面学的中间件和路由控制合起来,做一个真实的 A/B 测试场景。目标是对首页访问者随机分到 A、B 两个版本,并用 Cookie 记住分组,保证同一用户每次看到的版本一致。
先用 _routes.json 确保首页走函数。
{
"version": 1,
"include": ["/", "/api/*"],
"exclude": ["/assets/*"]
}
然后写最外层中间件,只对首页做分流。
// functions/_middleware.js
export async function onRequest(context) {
const url = new URL(context.request.url);
// 只对首页做 A/B 测试,其他路径直接放行
if (url.pathname !== "/") {
return context.next();
}
// 读取已有分组
const cookie = context.request.headers.get("cookie") || "";
const match = cookie.match(/variant=(A|B)/);
let variant = match ? match[1] : null;
let needSetCookie = false;
// 没有分组就随机分配
if (!variant) {
variant = Math.random() < 0.5 ? "A" : "B";
needSetCookie = true;
}
// 继续执行后续处理
const response = await context.next();
// 首次访问写入 Cookie,一年有效
if (needSetCookie) {
response.headers.set(
"Set-Cookie",
`variant=${variant}; Path=/; Max-Age=31536000`
);
}
// 标记分组,方便下游和监控识别
response.headers.set("X-Variant", variant);
return response;
}
几个关键点用表格理清。
| 步骤 | 做法 |
|---|---|
| 分流 | 用 Math.random 按比例分到 A 或 B |
| 粘性 | 用 variant Cookie 记住分组,同一用户稳定 |
| 传递 | 用 X-Variant 响应头把分组透出给前端和统计 |
| 范围 | 只对首页生效,其他路径直接 next 放行 |
前端的静态页面读 X-Variant 头决定渲染哪个版本,或者统计脚本读这个头记录分组数据,就能对比两个版本的真实效果。
中间件做 A/B 测试的好处是逻辑集中在一处,端点和静态页面都不用改,关掉测试只要删掉中间件即可。
中间件用 _middleware.js 定义,按目录决定作用域,外层先执行内层后执行,用 context.next() 串联。_routes.json 控制 Functions 的生效范围,收窄 include 能避免静态请求被算成函数调用。A/B 测试把分流、粘性、透出三件事组合起来,是中间件的典型实战。到这里 Pages Functions 的基础和中间件都讲完了,足够支撑大部分静态站点的动态需求。