Edge Runtime 与 Middleware

Edge Runtime 让代码在全球 CDN 边缘节点执行,延迟低至个位数毫秒。Middleware 在请求到达页面之前拦截处理——认证、重定向、地理路由、A/B 测试都在这里完成。本章讲清 Edge 的能力边界、Middleware 的实战模式和常见陷阱。

1. Edge Runtime 基础

1.1 什么是 Edge Runtime

Next.js 提供两种服务端运行时:

维度Node.js RuntimeEdge Runtime
运行位置单一区域(如 us-east-1)全球边缘节点(~30 个)
冷启动慢(~200ms)快(~1ms)
API完整 Node.js APIWeb API 子集
可用模块fs、crypto、net 等全部无 fs、无 net、无原生模块
最大执行时间60 秒25 秒
最大体积50MB4MB(Vercel)
数据库访问TCP 直连仅 HTTP 协议

Edge 的优势:用户请求就近处理,TTFB(首字节时间)从几百毫秒降到几十毫秒。

Edge 的限制:不能用 Node.js 原生模块(fsnetchild_process),不能 TCP 直连数据库,很多 npm 包不兼容。

1.2 选择运行时

在 Route Handler 或 Page 中声明运行时:

// Edge Runtime
export const runtime = 'edge'
 
// Node.js Runtime(默认)
export const runtime = 'nodejs'

什么时候用 Edge:

  • Middleware(强制 Edge)
  • 简单的 API 代理、重定向
  • 不需要数据库的计算(Token 校验、地理定位)
  • 配合 HTTP 协议数据库(Neon、PlanetScale)

什么时候用 Node.js:

  • 需要 TCP 连接数据库
  • 使用不兼容 Edge 的 npm 包(Prisma、Sharp)
  • 需要文件系统操作
  • 长时间执行的任务

1.3 Edge 兼容的数据库访问

在 Edge Runtime 中不能用 TCP 连接。需要用 HTTP 协议的数据库驱动(第 28 章详细讲过):

// app/api/edge-query/route.ts
export const runtime = 'edge'
 
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
 
const sql = neon(process.env.DATABASE_URL!)
const db = drizzle(sql)
 
export async function GET() {
  const result = await db.select().from(chats).limit(10)
  return Response.json(result)
}

2. Middleware

2.1 基础概念

Middleware 是 Next.js 中唯一在所有请求前执行的代码——在路由匹配、页面渲染之前运行。它强制运行在 Edge Runtime。

// middleware.ts — 项目根目录,只能有一个文件
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // 在这里拦截、修改、重定向请求
  return NextResponse.next()
}
 
// 配置匹配规则
export const config = {
  matcher: [
    // 匹配所有路径,排除静态文件和 API
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

2.2 Matcher 配置

matcher 决定哪些路径触发 Middleware——配置精准的 matcher 是性能关键,避免对静态资源执行 Middleware:

export const config = {
  matcher: [
    // 方式一:精确路径
    '/dashboard/:path*',
    '/api/:path*',
 
    // 方式二:正则排除(推荐)
    '/((?!_next/static|_next/image|favicon.ico|public/).*)',
 
    // 方式三:只匹配特定路径
    '/login',
    '/register',
  ],
}

2.3 认证保护

最常见的 Middleware 用途——未登录用户重定向到登录页:

// middleware.ts
import { auth } from '@/lib/auth'
 
export async function middleware(request: NextRequest) {
  const session = await auth()
  const { pathname } = request.nextUrl
 
  // 公开路径:登录、注册、首页
  const publicPaths = ['/login', '/register', '/', '/pricing']
  if (publicPaths.some((path) => pathname.startsWith(path))) {
    // 已登录用户访问登录页 → 跳转到仪表盘
    if (session && (pathname === '/login' || pathname === '/register')) {
      return NextResponse.redirect(new URL('/dashboard', request.url))
    }
    return NextResponse.next()
  }
 
  // 受保护路径:未登录 → 跳转到登录页
  if (!session) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('callbackUrl', pathname) // 登录后跳回原页面
    return NextResponse.redirect(loginUrl)
  }
 
  return NextResponse.next()
}
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api/webhook).*)'],
}
Middleware 中不要做重活

Middleware 在每个请求前执行——如果做数据库查询或复杂计算,会严重拖慢所有页面。认证检查应该用轻量的 JWT 验证或 Cookie 检查,不要在 Middleware 中查数据库。

2.4 请求头注入

Middleware 可以修改请求头——把信息传递给下游的 Server Component / Route Handler:

export function middleware(request: NextRequest) {
  const response = NextResponse.next()
 
  // 注入地理信息(Vercel 自动提供 geo 数据)
  response.headers.set('x-user-country', request.geo?.country ?? 'unknown')
  response.headers.set('x-user-city', request.geo?.city ?? 'unknown')
 
  // 注入请求路径(Server Component 中用 headers() 读取)
  response.headers.set('x-pathname', request.nextUrl.pathname)
 
  return response
}

Server Component 中读取:

import { headers } from 'next/headers'
 
export default async function Page() {
  const headerStore = await headers()
  const country = headerStore.get('x-user-country')
  const pathname = headerStore.get('x-pathname')
  // ...
}

3. 地理路由

3.1 基于国家的内容定制

Vercel 在 Edge 节点自动注入地理信息到 request.geo

export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? 'US'
 
  // 中国用户重定向到中文站
  if (country === 'CN' && !request.nextUrl.pathname.startsWith('/zh')) {
    return NextResponse.redirect(new URL(`/zh${request.nextUrl.pathname}`, request.url))
  }
 
  // 欧盟用户显示 Cookie 横幅
  const euCountries = ['DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'SE', 'PL']
  if (euCountries.includes(country)) {
    const response = NextResponse.next()
    response.headers.set('x-show-cookie-banner', 'true')
    return response
  }
 
  return NextResponse.next()
}

3.2 就近数据源

结合地理信息选择最近的数据库区域:

export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? 'US'
  const response = NextResponse.next()
 
  // 根据用户位置选择数据库区域
  const dbRegion = getClosestRegion(country)
  response.headers.set('x-db-region', dbRegion)
 
  return response
}
 
function getClosestRegion(country: string) {
  const asiaPacific = ['CN', 'JP', 'KR', 'SG', 'AU', 'IN']
  const europe = ['DE', 'FR', 'GB', 'IT', 'ES', 'NL']
  if (asiaPacific.includes(country)) return 'ap-southeast-1'
  if (europe.includes(country)) return 'eu-west-1'
  return 'us-east-1'
}

4. A/B 测试

Middleware 是实现 A/B 测试的理想位置——在请求到达页面之前决定用户看到哪个版本,且不影响缓存。

export function middleware(request: NextRequest) {
  // 检查用户是否已分配实验组
  let bucket = request.cookies.get('ab-experiment')?.value
 
  if (!bucket) {
    // 首次访问:随机分配 A 或 B
    bucket = Math.random() < 0.5 ? 'control' : 'variant'
  }
 
  const response = NextResponse.next()
 
  // 设置 Cookie 保持一致性(同一用户始终看到同一版本)
  response.cookies.set('ab-experiment', bucket, {
    maxAge: 60 * 60 * 24 * 30, // 30 天
    httpOnly: true,
  })
 
  // 通过 Header 传递给页面
  response.headers.set('x-ab-bucket', bucket)
 
  return response
}

4.2 用 Rewrite 实现不同页面

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/pricing') {
    const bucket = request.cookies.get('ab-experiment')?.value ?? 'control'
 
    // A 组看原版定价页,B 组看新版
    if (bucket === 'variant') {
      return NextResponse.rewrite(new URL('/pricing-v2', request.url))
    }
  }
 
  return NextResponse.next()
}

Rewrite 不会改变浏览器地址栏的 URL——用户都看到 /pricing,但实际渲染不同的页面。

5. Middleware 组合模式

实际项目中 Middleware 需要同时处理多个逻辑。推荐用责任链模式组织:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
type MiddlewareHandler = (
  request: NextRequest,
  response: NextResponse,
) => NextResponse | Response | undefined
 
// 各个中间件逻辑
const authMiddleware: MiddlewareHandler = (request, response) => {
  const token = request.cookies.get('session-token')?.value
  const isPublic = ['/login', '/register', '/'].includes(request.nextUrl.pathname)
 
  if (!token && !isPublic) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}
 
const geoMiddleware: MiddlewareHandler = (request, response) => {
  response.headers.set('x-user-country', request.geo?.country ?? 'unknown')
}
 
const abTestMiddleware: MiddlewareHandler = (request, response) => {
  if (!request.cookies.get('ab-experiment')) {
    response.cookies.set('ab-experiment', Math.random() < 0.5 ? 'control' : 'variant')
  }
}
 
// 组合执行
const middlewares: MiddlewareHandler[] = [authMiddleware, geoMiddleware, abTestMiddleware]
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next()
 
  for (const handler of middlewares) {
    const result = handler(request, response)
    if (result instanceof Response) return result // 短路:重定向等
  }
 
  return response
}
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|api/webhook).*)'],
}

6. 常见陷阱

6.1 Middleware 不能做的事

  • 查数据库(TCP):Edge 没有 TCP,只能用 HTTP 驱动
  • 使用 Node.js 模块fscrypto.createHash(可以用 crypto.subtle
  • 执行长时间操作:每次请求都会执行,必须快
  • 修改 Response Body:只能修改 Header、Cookie、重定向/重写

6.2 性能注意

  • 精准 matcher:不匹配静态资源(_next/static、图片等)
  • 避免外部请求:Middleware 中 fetch 第三方 API 会拖慢所有页面
  • JWT > 数据库查询:认证用 JWT 本地验证(~1ms),不要查数据库(~50ms)

6.3 调试技巧

export function middleware(request: NextRequest) {
  // 开发环境打印调试信息
  if (process.env.NODE_ENV === 'development') {
    console.log(`[Middleware] ${request.method} ${request.nextUrl.pathname}`)
  }
  // ...
}

本章小结

  • Edge Runtime:全球边缘执行,冷启动快,但 API 受限(无 fs/net)
  • Middleware:强制 Edge,每个请求前执行,适合认证、重定向、Header 注入
  • 地理路由:基于 request.geo 做国家级内容定制和数据源选择
  • A/B 测试:Cookie 分流 + Rewrite 实现,不影响缓存
  • 组合模式:责任链模式组织多个 Middleware 逻辑
  • 性能关键:精准 matcher + 避免外部请求 + JWT 本地验证

至此,第六篇「服务端能力」全部完成。下一篇将深入 AI 能力集成。