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)
正则 复杂匹配 `/(?en

四、请求处理 #

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