Skip to content

中间件

耶和博

3253字约11分钟

2024-08-10

中间件

中间件允许你在请求完成之前运行代码。然后,基于传入的请求,你可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。

中间件在缓存内容和路由匹配之前运行。有关更多详细信息,请参阅匹配路径


使用场景

将中间件集成到你的应用程序中可以显著提高性能、安全性和用户体验。中间件特别有效的一些常见场景包括:

  • 身份验证和授权:在授予对特定页面或 API 路由的访问权限之前,确保用户身份并检查会话 cookie。
  • 服务器端重定向:基于某些条件(例如,区域设置、用户角色)在服务器级别重定向用户。
  • 路径重写:通过根据请求属性动态重写到 API 路由或页面的路径来支持 A/B 测试、功能推出或遗留路径。
  • 机器人检测:通过检测和阻止机器人流量来保护你的资源。
  • 日志记录和分析:在页面或 API 处理之前捕获和分析请求数据以获取洞察。
  • 功能标记:动态启用或禁用功能,以实现无缝功能推出或测试。

认识到中间件可能不是最佳方法的情况同样重要。以下是一些需要注意的场景:

  • 复杂的数据获取和操作:中间件不是为直接数据获取或操作而设计的,这应该在路由处理程序或服务器端实用程序中完成。
  • 繁重的计算任务:中间件应该是轻量级的并快速响应,否则可能会导致页面加载延迟。繁重的计算任务或长时间运行的进程应该在专用的路由处理程序中完成。
  • 广泛的会话管理:虽然中间件可以管理基本的会话任务,但广泛的会话管理应由专用的身份验证服务或在路由处理程序中管理。
  • 直接数据库操作:不建议在中间件中执行直接数据库操作。数据库交互应在路由处理程序或服务器端实用程序中完成。

约定

使用项目根目录中的 middleware.ts (或 .js) 文件来定义中间件。例如,与 pagesapp 在同一级别,或者如果适用的话在 src 内。

使用项目根目录中的 middleware.ts (或 .js) 文件来定义中间件。例如,与 pagesapp 在同一级别,或者如果适用的话在 src 内。

提示

虽然每个项目只支持一个 middleware.ts 文件,但你仍然可以模块化地组织你的中间件逻辑。将中间件功能分解到单独的 .ts.js 文件中,并将它们导入到主 middleware.ts 文件中。这允许更清晰地管理特定路由的中间件,并在 middleware.ts 中集中控制。通过强制使用单个中间件文件,它简化了配置,防止了潜在的冲突,并通过避免多个中间件层优化了性能。


示例

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
// See "Matching Paths" below to learn more
export const config = {
  matcher: '/about/:path*',
}

匹配路径

中间件将被调用用于你项目中的每个路由。鉴于此,使用匹配器精确地定位或排除特定路由至关重要。以下是执行顺序:

  1. next.config.js 中的 headers
  2. next.config.js 中的 redirects
  3. 中间件 (rewrites, redirects 等)
  4. next.config.js 中的 beforeFiles (rewrites)
  5. 文件系统路由 (public/, _next/static/, pages/, app/ 等)
  6. next.config.js 中的 afterFiles (rewrites)
  7. 动态路由 (/blog/[slug])
  8. next.config.js 中的 fallback (rewrites)

有两种方法可以定义中间件将在哪些路径上运行:

  1. 自定义匹配器配置
  2. 条件语句

匹配器

matcher 允许你过滤中间件以在特定路径上运行。

middleware.js
export const config = {
  matcher: '/about/:path*',
}

你可以使用数组语法匹配单个路径或多个路径:

middleware.js
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

matcher 配置允许完整的正则表达式,因此支持像负向前瞻或字符匹配这样的匹配。这里是一个使用负向前瞻来匹配除特定路径之外的所有路径的示例:

middleware.js
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

你也可以使用 missinghas 数组,或两者的组合,为某些请求绕过中间件:

middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下开头的路径:
     * - api (API 路由)
     * - _next/static (静态文件)
     * - _next/image (图像优化文件)
     * - favicon.ico, sitemap.xml, robots.txt (元数据文件)
     */
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

提示

matcher 值需要是常量,以便它们可以在构建时进行静态分析。动态值(如变量)将被忽略。

配置的匹配器:

  1. 必须以 / 开头
  2. 可以包含命名参数: /about/:path 匹配 /about/a/about/b 但不匹配 /about/a/c
  3. 可以在命名参数上有修饰符(以 : 开头): /about/:path* 匹配 /about/a/b/c,因为 *零个或多个?零个或一个,+一个或多个
  4. 可以使用括号中的正则表达式: /about/(.*)/about/:path* 相同

path-to-regexp 文档中阅读更多详细信息。

提示

为了向后兼容,Next.js 总是将 /public 视为 /public/index。因此,/public/:path 的匹配器将匹配。

条件语句

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

NextResponse

NextResponse API 允许你:

  • redirect 传入请求到不同的 URL
  • rewrite 响应,显示给定的 URL
  • 为 API 路由、getServerSidePropsrewrite 目标设置请求头
  • 设置响应 cookie
  • 设置响应头

要从中间件生成响应,你可以:

  1. rewrite 到一个产生响应的路由(页面路由处理程序)
  2. 直接返回一个 NextResponse。参见生成响应

使用 Cookies

Cookies 是常规头。在 Request 上,它们存储在 Cookie 头中。在 Response 上,它们在 Set-Cookie 头中。Next.js 通过 NextRequestNextResponse 上的 cookies 扩展提供了一种方便的方式来访问和操作这些 cookies。

  1. 对于传入的请求,cookies 带有以下方法: getgetAllsetdelete cookies。你可以使用 has 检查 cookie 是否存在,或使用 clear 删除所有 cookies。
  2. 对于传出的响应,cookies 有以下方法: getgetAllsetdelete
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
  // Getting cookies from the request using the `RequestCookies` API
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
 
  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false
 
  // Setting cookies on the response using the `ResponseCookies` API
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
 
  return response
}

设置头

你可以使用 NextResponse API 设置请求和响应头(设置 请求 头从 Next.js v13.0.0 开始可用)。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // You can also set request headers in NextResponse.next
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  })
 
  // Set a new response header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

提示

避免设置大型头,因为这可能会导致 431 请求头字段太大 ,具体错误取决于您的后端 Web 服务器配置。

CORS

你可以在中间件中设置 CORS 头以允许跨域请求,包括简单预检请求

middleware.ts
import { NextRequest, NextResponse } from 'next/server'
 
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
 
const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
 
export function middleware(request: NextRequest) {
  // Check the origin from the request
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)
 
  // Handle preflighted requests
  const isPreflight = request.method === 'OPTIONS'
 
  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }
 
  // Handle simple requests
  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*',
}

提示

你可以为单个路由在路由处理程序中配置 CORS 头。


生成响应

你可以直接从中间件返回一个 ResponseNextResponse 实例来响应(从 Next.js v13.1.0 开始可用)。

middleware.ts
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
 
// Limit the middleware to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}
 
export function middleware(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

waitUntilNextFetchEvent

NextFetchEvent 对象扩展了原生 FetchEvent 对象,并包括 waitUntil() 方法。

waitUntil() 方法接受一个 promise 作为参数,并将中间件的生命周期扩展到 promise 解决为止。这对于在后台执行工作非常有用。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
 
export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch('https://my-analytics-platform.com', {
      method: 'POST',
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    })
  )
 
  return NextResponse.next()
}

高级中间件标志

v13.1 版本的 Next.js 中,引入了两个额外的中间件标志,skipMiddlewareUrlNormalizeskipTrailingSlashRedirect,用于处理高级用例。

skipTrailingSlashRedirect 禁用 Next.js 重定向以添加或删除尾部斜杠。这允许在中间件内自定义处理,以便为某些路径保持尾部斜杠,而不是其他路径,这可以使增量迁移更容易。

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}
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()
  }
 
  // apply trailing slash handling
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    return NextResponse.redirect(
      new URL(`${req.nextUrl.pathname}/`, req.nextUrl)
    )
  }
}

skipMiddlewareUrlNormalize 允许禁用 Next.js 中的 URL 规范化,使处理直接访问和客户端转换相同。在某些高级情况下,此选项提供了完全控制权,使用原始 URL。

next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}
middleware.js
export default async function middleware(req) {
  const { pathname } = req.nextUrl
 
  // GET /_next/data/build-id/hello.json
 
  console.log(pathname)
  // with the flag this now /_next/data/build-id/hello.json
  // without the flag this would be normalized to /hello
}

运行时

中间件当前仅支持边缘运行时。无法使用 Node.js 运行时。


版本历史

版本变更
v13.1.0添加了高级中间件标志
v13.0.0中间件可以修改请求头、响应头和发送响应
v12.2.0中间件已稳定,请参阅升级指南
v12.0.9在边缘运行时中强制使用绝对 URL (PR)
v12.0.0中间件 (Beta) 已添加