Edge Runtime 与 Middleware
Edge Runtime 让代码在全球 CDN 边缘节点执行,延迟低至个位数毫秒。Middleware 在请求到达页面之前拦截处理——认证、重定向、地理路由、A/B 测试都在这里完成。本章讲清 Edge 的能力边界、Middleware 的实战模式和常见陷阱。
1. Edge Runtime 基础
1.1 什么是 Edge Runtime
Next.js 提供两种服务端运行时:
| 维度 | Node.js Runtime | Edge Runtime |
|---|---|---|
| 运行位置 | 单一区域(如 us-east-1) | 全球边缘节点(~30 个) |
| 冷启动 | 慢(~200ms) | 快(~1ms) |
| API | 完整 Node.js API | Web API 子集 |
| 可用模块 | fs、crypto、net 等全部 | 无 fs、无 net、无原生模块 |
| 最大执行时间 | 60 秒 | 25 秒 |
| 最大体积 | 50MB | 4MB(Vercel) |
| 数据库访问 | TCP 直连 | 仅 HTTP 协议 |
Edge 的优势:用户请求就近处理,TTFB(首字节时间)从几百毫秒降到几十毫秒。
Edge 的限制:不能用 Node.js 原生模块(fs、net、child_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 在每个请求前执行——如果做数据库查询或复杂计算,会严重拖慢所有页面。认证检查应该用轻量的 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 测试的理想位置——在请求到达页面之前决定用户看到哪个版本,且不影响缓存。
4.1 基于 Cookie 的分流
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 模块:
fs、crypto.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 能力集成。