Next.js 中间件与边缘函数:从请求拦截到全球加速的深度实践
一、服务端逻辑的"最后一公里":为什么需要在边缘执行?
Web 应用的请求处理链路中,存在大量轻量但高频的操作——身份验证、A/B 测试分流、地域重定向、Bot 检测。这些操作的传统做法是在 Node.js 服务端或 API 路由中处理,但每次请求都要穿越到单一区域的源站,延迟动辄 100-300ms。对于全球用户而言,这种"所有请求都回源"的架构在延迟和可用性上都是不可接受的。
Next.js 中间件(Middleware)和边缘函数(Edge Functions)提供了一种在 CDN 边缘节点执行逻辑的方案。代码部署在全球 300+ 个 PoP 节点上,用户请求在最近的边缘节点即可完成处理,无需回源。这种架构将认证、重定向等逻辑的延迟从数百毫秒降低到个位数毫秒。
二、中间件执行模型与边缘运行时原理
Next.js 中间件的核心执行模型是"请求拦截 → 条件匹配 → 响应改写"。中间件在 Edge Runtime 中运行,这是一个基于 V8 的轻量运行时,不支持 Node.js 的全部 API(如 fs、crypto 的某些方法),但支持 Web 标准 API(Fetch、Request、Response)。
flowchart TD A[用户请求] --> B{CDN 边缘节点} B --> C[Middleware 执行] C --> D{匹配规则判断} D -->|认证失败| E[返回 401 / 重定向登录] D -->|地域分流| F[重写 URL 到对应语言版本] D -->|Bot 检测| G[返回验证页面 / Block] D -->|正常请求| H[继续到源站或缓存] H --> I[返回响应] E --> J[用户收到响应] F --> J G --> J I --> J关键设计约束:
- 执行时间限制:边缘函数的执行时间通常限制在 50ms 以内(Vercel 平台),复杂逻辑必须拆分
- 包体积限制:中间件打包后的体积不能超过 1MB(含依赖),需要精简依赖选择
- 无状态执行:边缘节点间不共享内存状态,缓存需依赖 KV 存储或 Cache API
三、生产级中间件实现与最佳实践
// middleware.ts — Next.js 边缘中间件 // 设计意图:在 CDN 边缘节点执行认证、分流和 Bot 检测, // 避免请求回源,将延迟控制在 10ms 以内 import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; // 轻量级 JWT 验证(不使用 jsonwebtoken,因其依赖 Node.js crypto) // 使用 Web Crypto API 实现,兼容 Edge Runtime async function verifyToken(token: string, secret: string): Promise<boolean> { try { const parts = token.split('.'); if (parts.length !== 3) return false; // 使用 Web Crypto API 验证签名 const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( 'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'] ); const signature = Uint8Array.from( atob(parts[2].replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0) ); const data = encoder.encode(`${parts[0]}.${parts[1]}`); const valid = await crypto.subtle.verify('HMAC', key, signature, data); return valid; } catch { return false; } } // Bot 检测:基于 User-Agent 和请求头特征 function isBot(request: NextRequest): boolean { const ua = request.headers.get('user-agent') || ''; const botPatterns = [ /bot/i, /crawler/i, /spider/i, /headless/i, /python-requests/i, /curl/i, /wget/i, ]; if (botPatterns.some(p => p.test(ua))) return true; // Headless Chrome 检测:缺少 WebDriver 标志但行为异常 const secChUa = request.headers.get('sec-ch-ua') || ''; if (secChUa.includes('HeadlessChrome')) return true; return false; } // 地域分流:根据请求 IP 所属国家重定向 function getLocaleRedirect(request: NextRequest): string | null { const country = request.geo?.country || 'US'; const localeMap: Record<string, string> = { CN: '/zh', JP: '/ja', KR: '/ko', DE: '/de', FR: '/fr', }; return localeMap[country] || null; } export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // === 规则 1:静态资源和 API 路由跳过中间件 === if ( pathname.startsWith('/_next') || pathname.startsWith('/api') || pathname.includes('.') ) { return NextResponse.next(); } // === 规则 2:Bot 检测与限流 === if (isBot(request)) { // 对爬虫返回轻量验证页面,而非完整渲染 const verifyUrl = request.nextUrl.clone(); verifyUrl.pathname = '/verify'; return NextResponse.rewrite(verifyUrl); } // === 规则 3:认证检查(仅保护 /dashboard 路径) === if (pathname.startsWith('/dashboard')) { const token = request.cookies.get('auth_token')?.value; if (!token) { const loginUrl = request.nextUrl.clone(); loginUrl.pathname = '/login'; loginUrl.searchParams.set('callbackUrl', pathname); return NextResponse.redirect(loginUrl); } // 在边缘节点验证 JWT,避免回源到认证服务 const isValid = await verifyToken(token, process.env.JWT_SECRET || ''); if (!isValid) { const loginUrl = request.nextUrl.clone(); loginUrl.pathname = '/login'; // 清除无效 token const response = NextResponse.redirect(loginUrl); response.cookies.delete('auth_token'); return response; } } // === 规则 4:地域分流(仅对首页生效) === if (pathname === '/') { const localePath = getLocaleRedirect(request); if (localePath) { const redirectUrl = request.nextUrl.clone(); redirectUrl.pathname = localePath; // 使用 rewrite 而非 redirect,URL 不变但内容切换 return NextResponse.rewrite(redirectUrl); } } // === 规则 5:A/B 测试分流 === if (pathname.startsWith('/pricing')) { const bucket = request.cookies.get('ab_bucket')?.value; if (!bucket) { // 基于用户 ID 哈希分桶,确保同一用户始终看到同一版本 const userId = request.cookies.get('user_id')?.value || crypto.randomUUID(); const hash = userId.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); const assignedBucket = hash % 2 === 0 ? 'A' : 'B'; const response = NextResponse.next(); response.cookies.set('ab_bucket', assignedBucket, { maxAge: 60 * 60 * 24 * 30, // 30 天 path: '/', sameSite: 'lax', }); return response; } // B 版本用户看到不同的定价页面 if (bucket === 'B') { const rewriteUrl = request.nextUrl.clone(); rewriteUrl.pathname = '/pricing-v2'; return NextResponse.rewrite(rewriteUrl); } } return NextResponse.next(); } // 精确配置匹配路径,避免不必要的中间件执行 export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico).*)', ], };四、边缘架构的 Trade-offs 与适用边界
冷启动延迟:边缘函数的首次调用存在冷启动问题,通常在 50-200ms 之间。虽然比传统 Serverless 冷启动快(V8 隔离而非容器),但对于要求 P99 < 10ms 的场景仍需关注。解决方案是保持函数活跃(定时 ping)或使用 Vercel 的 Edge Function 预热机制。
运行时限制:Edge Runtime 不支持 Node.js 的完整 API。无法使用fs、child_process、net等模块,也无法使用依赖这些模块的第三方库(如jsonwebtoken、mongoose)。在选型时必须确认所有依赖兼容 Web 标准 API,否则运行时会抛出异常。
调试与可观测性:边缘函数的调试比 Node.js 服务端困难得多。日志分散在 300+ 个 PoP 节点上,传统的集中式日志方案不适用。需要依赖平台提供的 Edge Logging(如 Vercel Edge Logs)或自建日志采集管线,将边缘日志汇总到中心化存储。
成本考量:边缘函数按调用次数和执行时间计费,高频轻量请求(如认证检查)的成本通常低于传统 Serverless,但涉及大量计算的场景(如复杂的数据转换)可能比在单一区域运行更贵,因为每个边缘节点都要执行一次。
五、总结
Next.js 中间件和边缘函数将 Web 应用的轻量级请求处理从源站下沉到 CDN 边缘,显著降低了认证、分流、Bot 检测等操作的延迟。核心价值在于"就近处理"——用户请求在最近的边缘节点完成逻辑判断,无需回源。但边缘运行时的 API 限制、冷启动延迟和调试困难是需要权衡的因素。在实际落地中,建议将中间件严格限定在"轻量决策"场景(认证、重定向、分流),将复杂业务逻辑保留在 Node.js API 路由或独立微服务中。随着 Edge Runtime 生态的成熟和 Web 标准 API 的完善,边缘函数的适用场景将持续扩展,成为现代 Web 架构的标准组件。