Next.js中间件 #
一、中间件概述 #
1.1 什么是中间件 #
中间件是在请求完成之前运行的代码,可以拦截、修改请求和响应:
text
请求 → 中间件 → 路由处理 → 响应
↓
可以修改/重定向
1.2 中间件位置 #
在项目根目录创建 middleware.ts:
text
project/
├── src/
│ ├── app/
│ └── middleware.ts # src目录内
├── middleware.ts # 根目录
└── package.json
1.3 基本结构 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.next()
}
export const config = {
matcher: '/about/:path*',
}
二、基本用法 #
2.1 简单中间件 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
console.log('请求路径:', request.nextUrl.pathname)
return NextResponse.next()
}
2.2 条件重定向 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url))
}
return NextResponse.next()
}
2.3 重写URL #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/blog')) {
return NextResponse.rewrite(new URL('/posts', request.url))
}
return NextResponse.next()
}
三、匹配器配置 #
3.1 单一路径 #
tsx
export const config = {
matcher: '/about',
}
3.2 多个路径 #
tsx
export const config = {
matcher: ['/about', '/contact'],
}
3.3 通配符匹配 #
tsx
export const config = {
matcher: '/api/:path*',
}
3.4 正则表达式 #
tsx
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
3.5 匹配器语法 #
| 语法 | 说明 | 示例 |
|---|---|---|
:path |
匹配单个段 | /blog/:slug |
:path* |
匹配多个段 | /api/:path* |
() |
可选段 | /blog(/:slug) |
| 正则 | 复杂匹配 | `/(? |
四、请求处理 #
4.1 请求对象 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const url = request.nextUrl
const pathname = url.pathname
const searchParams = url.searchParams
const method = request.method
const headers = request.headers
const cookies = request.cookies
console.log('路径:', pathname)
console.log('方法:', method)
console.log('查询参数:', searchParams.toString())
return NextResponse.next()
}
4.2 读取Cookie #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
const sessionId = request.cookies.get('session_id')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
4.3 读取Header #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const userAgent = request.headers.get('user-agent')
const authorization = request.headers.get('authorization')
const contentType = request.headers.get('content-type')
return NextResponse.next()
}
五、响应处理 #
5.1 设置Cookie #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.cookies.set('visited', 'true', {
path: '/',
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
secure: true,
sameSite: 'strict',
})
return response
}
5.2 设置Header #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'custom-value')
response.headers.set('Cache-Control', 'public, max-age=31536000')
return response
}
5.3 返回JSON响应 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/protected')) {
return NextResponse.json(
{ error: '未授权访问' },
{ status: 401 }
)
}
return NextResponse.next()
}
六、认证保护 #
6.1 简单认证 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
const isAuthenticated = !!token
const protectedPaths = ['/dashboard', '/profile', '/settings']
const isProtectedPath = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
)
if (isProtectedPath && !isAuthenticated) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*'],
}
6.2 角色权限 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const role = request.cookies.get('user_role')?.value
const adminPaths = ['/admin']
const isAdminPath = adminPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
)
if (isAdminPath && role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
return NextResponse.next()
}
6.3 JWT验证 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verify } from 'jsonwebtoken'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
const decoded = verify(token, process.env.JWT_SECRET!)
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', decoded.userId)
return NextResponse.next({
request: {
headers: requestHeaders,
},
})
} catch {
return NextResponse.redirect(new URL('/login', request.url))
}
}
七、国际化 #
7.1 语言检测 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['en', 'zh', 'ja']
const defaultLocale = 'en'
function getLocale(request: NextRequest): string {
const acceptLanguage = request.headers.get('accept-language') || ''
const preferredLocale = acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim().substring(0, 2))
.find(lang => locales.includes(lang))
return preferredLocale || defaultLocale
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
const pathnameIsMissingLocale = locales.every(
locale => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
)
if (pathnameIsMissingLocale) {
const locale = getLocale(request)
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
)
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
7.2 语言切换 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
const locale = request.nextUrl.pathname.split('/')[1]
response.cookies.set('locale', locale, {
path: '/',
maxAge: 60 * 60 * 24 * 365,
})
return response
}
八、日志记录 #
8.1 访问日志 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const start = Date.now()
const response = NextResponse.next()
const duration = Date.now() - start
console.log({
method: request.method,
path: request.nextUrl.pathname,
status: response.status,
duration: `${duration}ms`,
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || 'unknown',
})
return response
}
8.2 性能监控 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
const startTime = performance.now()
requestHeaders.set('x-start-time', startTime.toString())
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
const endTime = performance.now()
const duration = endTime - startTime
response.headers.set('x-response-time', `${duration.toFixed(2)}ms`)
return response
}
九、高级用法 #
9.1 链式中间件 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
type Middleware = (
request: NextRequest,
next: () => Promise<NextResponse>
) => Promise<NextResponse>
const authMiddleware: Middleware = async (request, next) => {
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return next()
}
const loggingMiddleware: Middleware = async (request, next) => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.nextUrl.pathname}`)
return next()
}
const corsMiddleware: Middleware = async (request, next) => {
if (request.method === 'OPTIONS') {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
const response = await next()
response.headers.set('Access-Control-Allow-Origin', '*')
return response
}
export async function middleware(request: NextRequest) {
const middlewares = [loggingMiddleware, authMiddleware, corsMiddleware]
const runMiddlewares = async (index: number): Promise<NextResponse> => {
if (index >= middlewares.length) {
return NextResponse.next()
}
return middlewares[index](request, () => runMiddlewares(index + 1))
}
return runMiddlewares(0)
}
9.2 A/B测试 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const variant = request.cookies.get('ab_variant')?.value
if (!variant) {
const newVariant = Math.random() > 0.5 ? 'A' : 'B'
const response = NextResponse.next()
response.cookies.set('ab_variant', newVariant, {
path: '/',
maxAge: 60 * 60 * 24 * 30,
})
if (newVariant === 'B') {
return NextResponse.rewrite(new URL('/variant-b', request.url))
}
return response
}
if (variant === 'B') {
return NextResponse.rewrite(new URL('/variant-b', request.url))
}
return NextResponse.next()
}
9.3 地理位置路由 #
tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.headers.get('x-vercel-ip-country') || 'US'
if (request.nextUrl.pathname === '/') {
if (country === 'CN') {
return NextResponse.redirect(new URL('/zh', request.url))
}
if (country === 'JP') {
return NextResponse.redirect(new URL('/ja', request.url))
}
}
return NextResponse.next()
}
十、最佳实践 #
10.1 性能优化 #
tsx
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|public).*)',
],
}
10.2 错误处理 #
tsx
export function middleware(request: NextRequest) {
try {
return handleMiddleware(request)
} catch (error) {
console.error('Middleware error:', error)
return NextResponse.next()
}
}
10.3 环境区分 #
tsx
export function middleware(request: NextRequest) {
if (process.env.NODE_ENV === 'development') {
console.log('Development middleware')
}
return NextResponse.next()
}
十一、总结 #
中间件要点:
| 要点 | 说明 |
|---|---|
| 位置 | 根目录或src目录 |
| 匹配器 | matcher配置 |
| 请求处理 | NextRequest对象 |
| 响应处理 | NextResponse对象 |
| 认证保护 | Cookie/Header验证 |
| 国际化 | 语言检测重定向 |
下一步,让我们学习页面与布局!
最后更新:2026-03-28