六维教程

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,后面的就不会执行了
上一篇
EdgeOne Makers Cloud Functions Go
下一篇
EdgeOne Makers Agents 概览