EdgeOne Makers 中间件
写函数的时候,有些逻辑是通用的,比如鉴权、日志记录、跨域处理。如果每个函数都写一遍,改一处就得改所有地方。中间件就是把这些通用逻辑抽出来,像流水线上的工位一样串起来,请求挨个经过每个工位,最终到达业务处理函数。
中间件是什么
中间件本质上就是一个函数,它接收请求,可以选择做一些处理,然后决定是把请求传给下一个中间件,还是直接返回响应。
打个比方: 你去医院看病,不是直接找专科医生。先在挂号处挂号(中间件 1),再去分诊台分科室(中间件 2),最后才到医生那里看病(业务函数)。每个环节只负责自己的事,互不干扰。
Edge Functions 和 Cloud Functions 都可以用中间件,但语法稍有不同。这篇主要基于 Edge Functions 的写法来讲,因为 Edge Functions 用标准 Web API,中间件的概念更直观。
第一个中间件
先写一个最简单的中间件,功能是给响应加一个自定义头。
// middleware.js
// 添加自定义响应头的中间件
export async function addHeader(request, env, ctx, next) {
// 调用 next 执行后续逻辑
const response = await next();
// 在返回的响应上加一个自定义头
response.headers.set('x-powered-by', 'EdgeOne Makers');
return response;
}
关键点在于 next 这个参数。调用 next() 就是把请求传递给下一个处理者。在 next() 之前做的事叫”前置处理”,之后做的叫”后置处理”。
请求进入
↓
前置处理(next 之前的代码)
↓
调用 next() → 传给下一个中间件或业务函数
↓
后置处理(next 之后的代码)
↓
返回响应
中间件链式调用
多个中间件按顺序串起来,就是链式调用。请求像过安检一样,一道一道过。
// middleware.js
// 中间件 1: 记录请求日志
export async function logger(request, env, ctx, next) {
const start = Date.now();
console.log(`请求开始: ${request.method} ${request.url}`);
// 调用下一个处理者
const response = await next();
const duration = Date.now() - start;
console.log(`请求结束: ${request.method} ${request.url} 耗时 ${duration}ms`);
return response;
}
// 中间件 2: CORS 跨域处理
export async function cors(request, env, ctx, next) {
// 预检请求直接返回
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET, POST, PUT, DELETE',
'access-control-allow-headers': 'content-type, authorization'
}
});
}
// 正常请求继续处理
const response = await next();
response.headers.set('access-control-allow-origin', '*');
return response;
}
// 中间件 3: 接口鉴权
export async function auth(request, env, ctx, next) {
// 从请求头里取 Token
const token = request.headers.get('authorization');
// 没有 Token 或者格式不对,直接拒绝
if (!token || !token.startsWith('Bearer ')) {
return new Response(JSON.stringify({ error: '未授权' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
}
// Token 存在就放行,交给下一个处理者
return next();
}
三个中间件的职责各不相同。
| 中间件 | 职责 | 前置处理 | 后置处理 |
|---|---|---|---|
| logger | 记录请求耗时 | 记录开始时间 | 计算耗时并打印日志 |
| cors | 处理跨域 | OPTIONS 预检直接返回 | 给响应加 CORS 头 |
| auth | 接口鉴权 | 检查 Token,不合法就拦截 | 没有(鉴权通过就放行) |
中间件组合使用
把上面的中间件组合起来,应用到具体的路由上。
// index.js
import { logger, cors, auth } from './middleware.js';
// 工具函数: 组合多个中间件
function compose(...middlewares) {
return async function(request, env, ctx) {
// 从最后一个中间件开始构建调用链
let index = 0;
async function dispatch(i) {
if (i <= index) {
throw new Error('next() 被多次调用');
}
index = i;
const fn = middlewares[i];
if (!fn) return;
await fn(request, env, ctx, function next() {
return dispatch(i + 1);
});
}
return dispatch(0);
};
}
// 公开路由: 只加日志和 CORS
const publicHandler = compose(logger, cors);
// 受保护路由: 日志 + CORS + 鉴权
const protectedHandler = compose(logger, cors, auth);
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname.startsWith('/api/public/')) {
return publicHandler(request, env, ctx);
}
if (url.pathname.startsWith('/api/protected/')) {
return protectedHandler(request, env, ctx);
}
return new Response('Not Found', { status: 404 });
}
};
实用中间件示例
频率限制中间件
防止同一个 IP 短时间内请求太多次。
// rate-limit.js
const requestCounts = new Map();
export async function rateLimit(request, env, ctx, next) {
// 获取客户端 IP
const ip = request.headers.get('cf-connecting-ip') || 'unknown';
// 获取当前计数
const count = requestCounts.get(ip) || 0;
// 超过限制就拒绝
if (count >= 60) {
return new Response(JSON.stringify({ error: '请求太频繁,请稍后再试' }), {
status: 429,
headers: {
'content-type': 'application/json',
'retry-after': '60'
}
});
}
// 计数加一
requestCounts.set(ip, count + 1);
// 一分钟后重置计数(简化处理,实际应该用更精确的方案)
setTimeout(() => {
const current = requestCounts.get(ip);
if (current <= 1) {
requestCounts.delete(ip);
} else {
requestCounts.set(ip, current - 1);
}
}, 60000);
return next();
}
缓存中间件
给响应加上缓存头,让浏览器或 CDN 缓存结果。
// cache.js
export async function cacheControl(request, env, ctx, next) {
const response = await next();
const url = new URL(request.url);
// 只对 GET 请求做缓存
if (request.method === 'GET') {
// 静态资源缓存一年
if (url.pathname.match(/\.(js|css|png|jpg|gif|svg)$/)) {
response.headers.set('cache-control', 'public, max-age=31536000');
}
// API 接口缓存 5 分钟
else if (url.pathname.startsWith('/api/')) {
response.headers.set('cache-control', 'public, max-age=300');
}
// 其他页面不缓存
else {
response.headers.set('cache-control', 'no-cache');
}
}
return response;
}
错误处理中间件
捕获后续处理中的异常,统一返回错误响应。
// error-handler.js
export async function errorHandler(request, env, ctx, next) {
try {
return await next();
} catch (err) {
console.error('处理出错:', err.message);
return new Response(JSON.stringify({
error: '服务器内部错误',
detail: err.message
}), {
status: 500,
headers: { 'content-type': 'application/json' }
});
}
}
中间件执行顺序
顺序很重要,不同的顺序会产生不同的效果。
| 场景 | 推荐顺序 | 原因 |
|---|---|---|
| 通用组合 | 错误处理 → 日志 → CORS → 鉴权 → 业务 | 错误处理兜底,日志记录全链路,CORS 让跨域请求能进来,鉴权后再做业务 |
| 不需要鉴权的路由 | 错误处理 → 日志 → CORS → 业务 | 跳过鉴权环节 |
| 需要限流的接口 | 错误处理 → 日志 → 限流 → CORS → 鉴权 → 业务 | 限流放在鉴权前面,避免非法请求也消耗鉴权资源 |
一个常见的错误是把鉴权放在日志前面,这样连被拒绝的请求都记录不到完整的链路信息。一般来说,日志和错误处理放最外层,业务相关的中间件放内层。
速查卡片
| 要点 | 说明 |
|---|---|
| 中间件是什么 | 通用逻辑抽出来的函数,串起来处理请求 |
| 核心机制 | 接收 next 参数,调用 next() 传给下一个处理者 |
| 前置处理 | next() 之前的代码,比如检查 Token |
| 后置处理 | next() 之后的代码,比如给响应加头 |
| 链式调用 | 多个中间件按顺序串联,像流水线 |
| 执行顺序 | 日志和错误处理放外层,业务中间件放内层 |
| 常用中间件 | 日志、CORS、鉴权、限流、缓存、错误处理 |
| 拦截请求 | 不调用 next(),直接返回 Response,后面的就不会执行了 |