Q17: 中间件(Middleware)的作用是什么?请列举几个常见的使用场景(身份验证、日志记录、重写、区域设置等)。它运行在哪个运行时?(Edge Runt

22 阅读2分钟

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*'],
}

总结

中间件的主要作用:

  1. 身份验证:检查用户权限和登录状态
  2. 日志记录:记录请求和响应信息
  3. 重写和重定向:修改请求路径或重定向到其他页面
  4. 国际化:处理多语言路由
  5. 安全:设置安全头和请求限制
  6. A/B 测试:根据用户分配不同的变体
  7. 性能优化:缓存和请求优化

Edge Runtime 特点

  1. 更快的启动时间:比 Node.js 运行时启动更快
  2. 全球分布:在多个地理位置运行
  3. 功能限制:不能使用 Node.js API,只能使用 Web APIs
  4. 内存限制:内存使用更少,但功能有限

中间件是 Next.js 中处理请求预处理逻辑的强大工具,特别适合身份验证、日志记录、重写等场景。