什么是 Next.js Middleware?
什么是 Next.js Middleware?
Next.js Middleware(在 v16.0.0 后更名为 Proxy)是一个强大的功能,允许你在请求完成之前在服务器上运行代码。基于传入的请求,你可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。
Middleware 在路由渲染之前执行,特别适合实现自定义服务器端逻辑,如身份验证、日志记录或处理重定向。
基础配置
1. 创建 Middleware 文件
在项目根目录(或 src 目录内)创建 middleware.ts 或 middleware.js 文件,与 pages 或 app 目录同级。
```typescript
// middleware.ts
import { NextResponse, NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}
```
2. 基本结构
Middleware 文件必须导出:
- middleware 函数:处理请求的主函数
- config 对象(可选):配置 middleware 应用的路径
Matcher 配置详解
单路径匹配
```javascript
export const config = {
matcher: '/about'
}
```
多路径匹配
```javascript
export const config = {
matcher: ['/about', '/contact', '/dashboard/:path*']
}
```
使用正则表达式
排除特定路径:
```javascript
export const config = {
matcher: [
// 排除 API 路由、静态文件、图片优化和 .png 文件
'/((?!api|_next/static|_next/image|.\\.png$).)',
],
}
```
高级 Matcher 配置
```javascript
export const config = {
matcher: [
{
source: '/api/:path*',
locale: false, // 忽略基于语言环境的路由
has: [
{ type: 'header', key: 'Authorization', value: 'Bearer Token' },
{ type: 'query', key: 'userId', value: '123' },
],
missing: [
{ type: 'cookie', key: 'session', value: 'active' }
],
},
],
}
```
Matcher 路径模式规则
- 必须以
/开头 - 命名参数:
/about/:path匹配/about/a和/about/b - 修饰符:
*= 零个或多个:/about/:path*匹配/about/a/b/c?= 零个或一个+= 一个或多个
- 正则表达式:
/about/(.*)等同于/about/:path* - 锚定到路径开头:
/about匹配/about和/about/team,但不匹配/blog/about
常见使用场景
1. 身份验证
```typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
}
```
2. 国际化重定向
```typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'
if (country === 'CN') {
return NextResponse.redirect(new URL('/zh-cn', request.url))
}
if (country === 'JP') {
return NextResponse.redirect(new URL('/ja', request.url))
}
return NextResponse.next()
}
```
3. A/B 测试
```typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const bucket = request.cookies.get('bucket')
if (!bucket) {
const newBucket = Math.random() < 0.5 ? 'a' : 'b'
const response = NextResponse.next()
response.cookies.set('bucket', newBucket)
return response
}
if (bucket.value === 'b') {
return NextResponse.rewrite(new URL('/experiment-b', request.url))
}
return NextResponse.next()
}
```
4. API 限流
```typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const rateLimit = new Map()
export function middleware(request: NextRequest) {
const ip = request.ip || 'unknown'
const now = Date.now()
const windowMs = 60000 // 1 分钟
const maxRequests = 10
const requests = rateLimit.get(ip) || []
const recentRequests = requests.filter((time: number) => now - time < windowMs)
if (recentRequests.length >= maxRequests) {
return new NextResponse('Too Many Requests', { status: 429 })
}
recentRequests.push(now)
rateLimit.set(ip, recentRequests)
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*'
}
```
5. 日志记录
```typescript
import { NextResponse } from 'next/server'
import type { NextRequest, NextFetchEvent } from 'next/server'
export function middleware(request: NextRequest, event: NextFetchEvent) {
// 使用 waitUntil 在后台记录日志
event.waitUntil(
fetch('https://analytics.example.com/log', {
method: 'POST',
body: JSON.stringify({
pathname: request.nextUrl.pathname,
timestamp: new Date().toISOString(),
userAgent: request.headers.get('user-agent'),
}),
})
)
return NextResponse.next()
}
```
Cookie 操作
读取 Cookie
```typescript
export function middleware(request: NextRequest) {
// 获取单个 cookie
const cookie = request.cookies.get('session')
console.log(cookie) // { name: 'session', value: 'abc123', Path: '/' }
// 获取所有 cookies
const allCookies = request.cookies.getAll()
// 检查 cookie 是否存在
const hasSession = request.cookies.has('session')
return NextResponse.next()
}
```
设置 Cookie
```typescript
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// 简单设置
response.cookies.set('theme', 'dark')
// 详细设置
response.cookies.set({
name: 'session',
value: 'abc123',
path: '/',
maxAge: 3600,
httpOnly: true,
secure: true,
sameSite: 'strict'
})
return response
}
```
删除 Cookie
```typescript
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// 删除单个 cookie
response.cookies.delete('session')
// 清除所有 cookies
request.cookies.clear()
return response
}
```
Header 操作
设置请求 Header
```typescript
export function middleware(request: NextRequest) {
// 克隆请求 headers
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-custom-header', 'my-value')
requestHeaders.set('x-user-ip', request.ip || 'unknown')
// 传递给下游
return NextResponse.next({
request: {
headers: requestHeaders,
},
})
}
```
设置响应 Header
```typescript
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// 设置响应 header
response.headers.set('x-custom-header', 'my-value')
response.headers.set('x-response-time', Date.now().toString())
return response
}
```
CORS 配置
```typescript
import { NextRequest, NextResponse } from 'next/server'
const allowedOrigins = ['https://example.com', 'https://app.example.com']
const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)
// 处理预检请求
if (request.method === 'OPTIONS') {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}
// 处理普通请求
const response = NextResponse.next()
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}
Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/api/:path*',
}
```
条件路由
基于路径的条件
```typescript
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/admin')) {
// 管理员路由逻辑
return checkAdminAuth(request)
}
if (pathname.startsWith('/api')) {
// API 路由逻辑
return handleApiRequest(request)
}
if (pathname.startsWith('/blog')) {
// 博客路由逻辑
return NextResponse.rewrite(new URL(`/posts${pathname}`, request.url))
}
return NextResponse.next()
}
```
基于用户角色的路由
```typescript
export function middleware(request: NextRequest) {
const userRole = request.cookies.get('role')?.value
const { pathname } = request.nextUrl
// 管理员专属路由
if (pathname.startsWith('/admin') && userRole !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// VIP 专属内容
if (pathname.startsWith('/premium') && userRole !== 'vip') {
return NextResponse.redirect(new URL('/upgrade', request.url))
}
return NextResponse.next()
}
```
重写和重定向
重写(Rewrite)
重写会改变 URL 的内部处理,但浏览器地址栏不变:
```typescript
export function middleware(request: NextRequest) {
// 将 /old-blog/post-1 重写为 /blog/post-1
if (request.nextUrl.pathname.startsWith('/old-blog')) {
const newPath = request.nextUrl.pathname.replace('/old-blog', '/blog')
return NextResponse.rewrite(new URL(newPath, request.url))
}
return NextResponse.next()
}
```
重定向(Redirect)
重定向会改变浏览器地址栏的 URL:
```typescript
export function middleware(request: NextRequest) {
// 永久重定向(301)
if (request.nextUrl.pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url), 301)
}
// 临时重定向(302,默认)
if (request.nextUrl.pathname === '/temp') {
return NextResponse.redirect(new URL('/temporary-page', request.url))
}
return NextResponse.next()
}
```
直接响应
从 Next.js v13.1.0 开始,可以直接从 middleware 返回响应:
```typescript
export function middleware(request: NextRequest) {
const isAuthenticated = checkAuth(request)
if (!isAuthenticated) {
return Response.json(
{ success: false, message: 'Authentication failed' },
{ status: 401 }
)
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
```
执行顺序
Middleware 会在每个路由上调用,执行顺序如下:
next.config.js中的headersnext.config.js中的redirects- Middleware(
rewrites、redirects等) next.config.js中的beforeFiles(rewrites)- 文件系统路由(
public/、_next/static/、pages/、app/等) next.config.js中的afterFiles(rewrites)- 动态路由(
/blog/[slug]) next.config.js中的fallback(rewrites)
高级配置
跳过尾部斜杠重定向
```javascript
// next.config.js
module.exports = {
skipTrailingSlashRedirect: true,
}
```
```javascript
// middleware.js
const legacyPrefixes = ['/docs', '/blog']
export default async function middleware(req) {
const { pathname } = req.nextUrl
if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
}
// 应用尾部斜杠处理
if (!pathname.endsWith('/') && !pathname.match(/\.[\w]+/)) {
return NextResponse.redirect(
new URL(\`\{req.nextUrl.pathname}/`, req.nextUrl)
)
}
}
```
跳过 URL 规范化
```javascript
// next.config.js
module.exports = {
skipMiddlewareUrlNormalize: true,
}
```
性能优化建议
1. 精确的 Matcher
使用精确的 matcher 避免不必要的执行:
```javascript
// ❌ 不好 - 会在所有路由上执行
export const config = {
matcher: '/:path*'
}
// ✅ 好 - 只在需要的路由上执行
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*']
}
```
2. 早期返回
尽早返回,避免不必要的处理:
```typescript
export function middleware(request: NextRequest) {
// 快速路径 - 静态资源直接通过
if (request.nextUrl.pathname.startsWith('/_next')) {
return NextResponse.next()
}
// 其他逻辑...
}
```
3. 避免重量级操作
Middleware 应该快速执行,避免:
- 复杂的数据库查询
- 大量的外部 API 调用
- 重量级的计算
使用 waitUntil 处理后台任务:
```typescript
export function middleware(request: NextRequest, event: NextFetchEvent) {
// 后台任务不会阻塞响应
event.waitUntil(
logAnalytics(request)
)
return NextResponse.next()
}
```
调试技巧
1. 添加日志
```typescript
export function middleware(request: NextRequest) {
console.log('Middleware executed for:', request.nextUrl.pathname)
console.log('Headers:', Object.fromEntries(request.headers))
console.log('Cookies:', request.cookies.getAll())
return NextResponse.next()
}
```
2. 添加调试 Header
```typescript
export function middleware(request: NextRequest) {
const response = NextResponse.next()
if (process.env.NODE_ENV === 'development') {
response.headers.set('x-middleware-executed', 'true')
response.headers.set('x-pathname', request.nextUrl.pathname)
}
return response
}
```
常见问题
1. Middleware 不执行
检查项:
- 文件位置是否正确(项目根目录或 src 目录)
- Matcher 配置是否正确
- 是否有语法错误
2. 无限重定向
**原因:**重定向目标也匹配 matcher
解决:
```typescript
export function middleware(request: NextRequest) {
// ❌ 错误 - 会导致无限重定向
if (!isAuthenticated(request)) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: '/:path*' // 包括 /login
}
// ✅ 正确 - 排除登录页
export const config = {
matcher: '/((?!login).*)'
}
```
3. Cookie 未设置
**原因:**忘记返回修改后的响应
解决:
```typescript
// ❌ 错误
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.cookies.set('theme', 'dark')
// 忘记返回 response
}
// ✅ 正确
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.cookies.set('theme', 'dark')
return response // 必须返回
}
```
最佳实践
- 使用 TypeScript:获得更好的类型安全和 IDE 支持
- 精确的 Matcher:只在需要的路由上执行
- 早期返回:尽快返回,避免不必要的处理
- 后台任务:使用
waitUntil处理非关键任务 - 错误处理:添加 try-catch 避免 middleware 崩溃
- 测试:为 middleware 编写单元测试
- 文档:注释复杂的逻辑和业务规则
总结
Next.js Middleware 是一个强大的工具,可以在请求到达页面之前进行各种处理。通过合理配置 matcher 和使用适当的 API,你可以实现:
- ✅ 身份验证和授权
- ✅ 国际化和地理位置路由
- ✅ A/B 测试和功能标志
- ✅ API 限流和安全
- ✅ 日志和分析
- ✅ 重写和重定向
- ✅ CORS 处理
记住:Middleware 应该快速执行,专注于路由和请求/响应修改,避免重量级操作。
相关资源: