Next.js 面试题详细答案 - Q17
Q17: 中间件(Middleware)的作用是什么?请列举几个常见的使用场景(身份验证、日志记录、重写、区域设置等)。它运行在哪个运行时?(Edge Runtime)
中间件概述
中间件是 Next.js 中在请求完成之前运行的代码,它可以在请求和响应之间执行逻辑,如身份验证、日志记录、重写等。
基本语法
1. 创建中间件
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// 中间件逻辑
return NextResponse.next()
}
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*'],
}
2. 中间件配置
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
// 中间件逻辑
console.log('请求路径:', pathname)
return NextResponse.next()
}
export const config = {
// 匹配特定路径
matcher: ['/api/:path*', '/dashboard/:path*', '/admin/:path*'],
// 或者使用函数
matcher: (pathname) => {
return pathname.startsWith('/api') || pathname.startsWith('/dashboard')
},
}
常见使用场景
1. 身份验证
// middleware.js
import { NextResponse } from 'next/server'
import { verifyToken } from './lib/auth'
export async function middleware(request) {
const { pathname } = request.nextUrl
// 检查是否需要身份验证
if (pathname.startsWith('/dashboard') || pathname.startsWith('/admin')) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
const user = await verifyToken(token)
if (!user) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 检查用户权限
if (pathname.startsWith('/admin') && user.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
// 将用户信息添加到请求头
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', user.id)
requestHeaders.set('x-user-role', user.role)
return NextResponse.next({
request: {
headers: requestHeaders,
},
})
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
2. 日志记录
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname, searchParams } = request.nextUrl
const start = Date.now()
// 记录请求信息
console.log({
method: request.method,
pathname,
searchParams: searchParams.toString(),
userAgent: request.headers.get('user-agent'),
ip:
request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip'),
timestamp: new Date().toISOString(),
})
const response = NextResponse.next()
// 记录响应时间
const duration = Date.now() - start
response.headers.set('x-response-time', `${duration}ms`)
return response
}
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*'],
}
3. 重写和重定向
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
// 重写路径
if (pathname.startsWith('/old-blog/')) {
const newPath = pathname.replace('/old-blog/', '/blog/')
return NextResponse.rewrite(new URL(newPath, request.url))
}
// 重定向
if (pathname === '/old-home') {
return NextResponse.redirect(new URL('/', request.url))
}
// 条件重定向
if (pathname.startsWith('/blog/') && pathname.endsWith('.html')) {
const newPath = pathname.replace('.html', '')
return NextResponse.redirect(new URL(newPath, request.url), 301)
}
return NextResponse.next()
}
export const config = {
matcher: ['/old-blog/:path*', '/old-home', '/blog/:path*.html'],
}
4. 区域设置和国际化
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
// 检查路径是否已经包含语言代码
const pathnameHasLocale = ['en', 'zh', 'ja'].some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (!pathnameHasLocale) {
// 从 Accept-Language 头获取首选语言
const acceptLanguage = request.headers.get('accept-language')
const preferredLocale = getPreferredLocale(acceptLanguage)
// 重定向到带语言代码的路径
return NextResponse.redirect(
new URL(`/${preferredLocale}${pathname}`, request.url)
)
}
return NextResponse.next()
}
function getPreferredLocale(acceptLanguage) {
if (!acceptLanguage) return 'en'
const languages = acceptLanguage
.split(',')
.map((lang) => lang.split(';')[0].trim())
for (const lang of languages) {
if (lang.startsWith('zh')) return 'zh'
if (lang.startsWith('ja')) return 'ja'
if (lang.startsWith('en')) return 'en'
}
return 'en'
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
5. 请求限制和速率限制
// middleware.js
import { NextResponse } from 'next/server'
// 简单的内存存储(生产环境应使用 Redis 等)
const rateLimitMap = new Map()
export function middleware(request) {
const { pathname } = request.nextUrl
const ip =
request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown'
// 只对 API 路由应用速率限制
if (pathname.startsWith('/api/')) {
const now = Date.now()
const windowMs = 60 * 1000 // 1分钟
const maxRequests = 100 // 每分钟最多100个请求
const key = `${ip}:${pathname}`
const requests = rateLimitMap.get(key) || []
// 清理过期的请求记录
const validRequests = requests.filter((time) => now - time < windowMs)
if (validRequests.length >= maxRequests) {
return new NextResponse('Too Many Requests', { status: 429 })
}
// 记录当前请求
validRequests.push(now)
rateLimitMap.set(key, validRequests)
// 添加速率限制头
const response = NextResponse.next()
response.headers.set('X-RateLimit-Limit', maxRequests.toString())
response.headers.set(
'X-RateLimit-Remaining',
(maxRequests - validRequests.length).toString()
)
response.headers.set(
'X-RateLimit-Reset',
new Date(now + windowMs).toISOString()
)
return response
}
return NextResponse.next()
}
export const config = {
matcher: ['/api/:path*'],
}
6. 安全头设置
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const response = NextResponse.next()
// 设置安全头
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'origin-when-cross-origin')
response.headers.set('X-XSS-Protection', '1; mode=block')
// 设置 CSP
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;"
)
// 设置 HSTS
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
)
return response
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
7. A/B 测试
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
// 只对特定页面进行 A/B 测试
if (pathname === '/') {
const userId = request.cookies.get('user-id')?.value || generateUserId()
const variant = getABTestVariant(userId)
// 设置变体 cookie
const response = NextResponse.next()
response.cookies.set('user-id', userId, { maxAge: 30 * 24 * 60 * 60 }) // 30天
response.cookies.set('ab-variant', variant, { maxAge: 30 * 24 * 60 * 60 })
return response
}
return NextResponse.next()
}
function generateUserId() {
return Math.random().toString(36).substring(2, 15)
}
function getABTestVariant(userId) {
// 简单的哈希函数来确定变体
const hash = userId.split('').reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0)
return a & a
}, 0)
return Math.abs(hash) % 2 === 0 ? 'A' : 'B'
}
export const config = {
matcher: ['/'],
}
Edge Runtime
1. 中间件运行环境
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// 中间件在 Edge Runtime 中运行
// 这意味着:
// 1. 更快的启动时间
// 2. 更小的内存占用
// 3. 全球分布
// 4. 但功能有限(不能使用 Node.js API)
const { pathname } = request.nextUrl
// 可以使用的功能:
// - Web APIs
// - fetch
// - Headers
// - URL
// - 等等
console.log('Edge Runtime 中的中间件')
return NextResponse.next()
}
export const config = {
matcher: ['/api/:path*'],
}
2. Edge Runtime 限制
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// ❌ 不能使用 Node.js API
// const fs = require('fs') // 错误
// const path = require('path') // 错误
// ✅ 可以使用 Web APIs
const url = new URL(request.url)
const headers = new Headers(request.headers)
// ✅ 可以使用 fetch
// const response = await fetch('https://api.example.com')
// ✅ 可以使用 crypto
const encoder = new TextEncoder()
const data = encoder.encode('hello')
return NextResponse.next()
}
export const config = {
matcher: ['/api/:path*'],
}
最佳实践
1. 性能优化
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
// 尽早返回,避免不必要的处理
if (pathname.startsWith('/_next/') || pathname.startsWith('/favicon.ico')) {
return NextResponse.next()
}
// 缓存计算结果
const cacheKey = `middleware:${pathname}`
// 使用缓存逻辑...
return NextResponse.next()
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
2. 错误处理
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
try {
const { pathname } = request.nextUrl
// 中间件逻辑
if (pathname.startsWith('/admin')) {
// 身份验证逻辑
}
return NextResponse.next()
} catch (error) {
console.error('中间件错误:', error)
// 返回错误响应或重定向到错误页面
return NextResponse.redirect(new URL('/error', request.url))
}
}
export const config = {
matcher: ['/admin/:path*'],
}
总结
中间件的主要作用:
- 身份验证:检查用户权限和登录状态
- 日志记录:记录请求和响应信息
- 重写和重定向:修改请求路径或重定向到其他页面
- 国际化:处理多语言路由
- 安全:设置安全头和请求限制
- A/B 测试:根据用户分配不同的变体
- 性能优化:缓存和请求优化
Edge Runtime 特点:
- 更快的启动时间:比 Node.js 运行时启动更快
- 全球分布:在多个地理位置运行
- 功能限制:不能使用 Node.js API,只能使用 Web APIs
- 内存限制:内存使用更少,但功能有限
中间件是 Next.js 中处理请求预处理逻辑的强大工具,特别适合身份验证、日志记录、重写等场景。