跳至内容
文档错误中间件升级指南

中间件升级指南

在我们努力改进中间件以实现通用可用性 (GA) 的过程中,我们根据您的反馈对中间件 API(以及您在应用程序中定义中间件的方式)进行了一些更改。

本升级指南将帮助您了解这些更改、更改的原因以及如何将您现有的中间件迁移到新的 API。本指南适用于以下 Next.js 开发人员

  • 目前正在使用 Beta 版 Next.js 中间件功能
  • 选择升级到 Next.js 的下一个稳定版本 (v12.2)

您可以使用最新版本 (npm i next@latest) 立即开始升级您的中间件用法。

注意:本指南中描述的这些更改包含在 Next.js 12.2 中。您可以保留您当前的站点结构,包括嵌套中间件,直到您迁移到 12.2(或 Next.js 的 canary 版本)。

如果您已配置 ESLint,则需要运行 npm i eslint-config-next@latest --save-dev 以升级您的 ESLint 配置,以确保使用与 Next.js 版本相同的版本。您可能还需要重新启动 VSCode 以使更改生效。

在 Vercel 上使用 Next.js 中间件

如果您在 Vercel 上使用 Next.js,则您使用中间件的现有部署将继续工作,并且您可以继续使用中间件部署您的站点。当您将站点升级到 Next.js 的下一个稳定版本 (v12.2) 时,您需要遵循本升级指南来更新您的中间件。

重大更改

  1. 不支持嵌套中间件
  2. 不支持响应体
  3. Cookies API 重构
  4. 新的用户代理助手
  5. 不再提供页面匹配数据
  6. 在内部 Next.js 请求上执行中间件

不支持嵌套中间件

更改摘要

  • 在您的 pages 文件夹旁边定义一个中间件文件
  • 无需在文件名前添加下划线
  • 可以使用自定义匹配器来使用导出的配置对象定义匹配路由

说明

以前,您可以在任何级别的 pages 目录下创建一个 _middleware.ts 文件。中间件执行基于其创建的文件路径。

根据客户反馈,我们已将此 API 替换为单个根中间件,它提供了以下改进

  • 更快的执行速度和更低的延迟:使用嵌套中间件,单个请求可能会调用多个中间件函数。单个中间件意味着单个函数执行,效率更高。
  • 成本更低:中间件的使用按调用次数计费。使用嵌套中间件,单个请求可能会调用多个中间件函数,这意味着每个请求都会产生多次中间件费用。单个中间件意味着每个请求只调用一次,因此更具成本效益。
  • 中间件可以方便地根据除路由之外的其他内容进行过滤:使用嵌套中间件,中间件文件位于 pages 目录中,中间件根据请求路径执行。通过迁移到单个根中间件,您仍然可以根据请求路径执行代码,但现在您可以更方便地根据其他条件(如 cookies 或请求头的存在)执行中间件。

  • 确定性执行顺序:使用嵌套中间件时,单个请求可能会匹配多个中间件函数。例如,对/dashboard/users/*的请求将调用在/dashboard/users/_middleware.ts/dashboard/_middleware.js中定义的中间件。但是,执行顺序难以推断。迁移到单个根中间件可以更明确地定义执行顺序。
  • 支持 Next.js 布局 (RFC):迁移到单个根中间件有助于支持 Next.js 中新的布局 (RFC)

如何升级

您应该在您的应用程序中声明一个中间件文件,该文件应位于pages目录旁边,并且无需_前缀命名。您的中间件文件仍然可以使用.ts.js扩展名。

中间件将为应用程序中的每个路由调用,并且可以使用自定义匹配器来定义匹配过滤器。以下是一个中间件的示例,该中间件会触发/about/*/dashboard/:path*,自定义匹配器在导出的配置对象中定义

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  return NextResponse.rewrite(new URL('/about-2', request.url))
}
 
// Supports both a single string value or an array of matchers
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

匹配器配置还允许使用完整的正则表达式,因此支持诸如否定前瞻或字符匹配之类的匹配。此处显示了一个否定前瞻的示例,用于匹配除特定路径之外的所有路径

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

虽然配置选项是首选,因为它不会在每个请求上调用,但您也可以使用条件语句仅在匹配特定路径时运行中间件。使用条件语句的一个优点是定义中间件执行时的显式顺序。以下示例显示了如何合并两个以前嵌套的中间件

middleware.ts
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    // This logic is only applied to /about
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    // This logic is only applied to /dashboard
  }
}

无响应主体

更改摘要

  • 中间件不再可以生成响应主体
  • 如果您的中间件确实响应了主体,则会抛出运行时错误
  • 迁移到使用rewrite/redirect到处理响应的页面/API

说明

为了尊重客户端和服务器端导航之间的差异,并帮助确保开发人员不会构建不安全的中间件,我们正在删除在中间件中发送响应主体的能力。这确保了中间件仅用于rewriteredirect或修改传入请求(例如设置cookie)。

以下模式将不再有效

new Response('a text value')
new Response(streamOrBuffer)
new Response(JSON.stringify(obj), { headers: 'application/json' })
NextResponse.json()

如何升级

对于使用中间件进行响应(例如授权)的情况,您应该迁移到使用rewrite/redirect到显示授权错误、登录表单或API路由的页面。

之前

pages/_middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { isAuthValid } from './lib/auth'
 
export function middleware(request: NextRequest) {
  // Example function to validate auth
  if (isAuthValid(request)) {
    return NextResponse.next()
  }
 
  return NextResponse.json({ message: 'Auth required' }, { status: 401 })
}

之后

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { isAuthValid } from './lib/auth'
 
export function middleware(request: NextRequest) {
  // Example function to validate auth
  if (isAuthValid(request)) {
    return NextResponse.next()
  }
 
  const loginUrl = new URL('/login', request.url)
  loginUrl.searchParams.set('from', request.nextUrl.pathname)
 
  return NextResponse.redirect(loginUrl)
}

边缘 API 路由

如果您之前使用中间件将标头转发到外部 API,您现在可以使用边缘 API 路由

pages/api/proxy.ts
import { type NextRequest } from 'next/server'
 
export const config = {
  runtime: 'edge',
}
 
export default async function handler(req: NextRequest) {
  const authorization = req.cookies.get('authorization')
  return fetch('https://backend-api.com/api/protected', {
    method: req.method,
    headers: {
      authorization,
    },
    redirect: 'manual',
  })
}

Cookie API 重构

更改摘要

新增移除
cookies.setcookie
cookies.deleteclearCookie
cookies.getWithOptionscookies

说明

根据 Beta 反馈,我们正在更改NextRequestNextResponse中的 Cookie API 以使其更符合get/set模型。Cookies API 扩展了 Map,包括entriesvalues等方法。

如何升级

NextResponse现在有一个带有以下内容的cookies实例:

  • cookies.delete
  • cookies.set
  • cookies.getWithOptions

以及来自Map的其他扩展方法。

之前

// pages/_middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // create an instance of the class to access the public methods. This uses `next()`,
  // you could use `redirect()` or `rewrite()` as well
  let response = NextResponse.next()
  // get the cookies from the request
  let cookieFromRequest = request.cookies['my-cookie']
  // set the `cookie`
  response.cookie('hello', 'world')
  // set the `cookie` with options
  const cookieWithOptions = response.cookie('hello', 'world', {
    path: '/',
    maxAge: 1000 * 60 * 60 * 24 * 7,
    httpOnly: true,
    sameSite: 'strict',
    domain: 'example.com',
  })
  // clear the `cookie`
  response.clearCookie('hello')
 
  return response
}

之后

middleware.ts
export function middleware() {
  const response = new NextResponse()
 
  // set a cookie
  response.cookies.set('vercel', 'fast')
 
  // set another cookie with options
  response.cookies.set('nextjs', 'awesome', { path: '/test' })
 
  // get all the details of a cookie
  const { value, ...options } = response.cookies.getWithOptions('vercel')
  console.log(value) // => 'fast'
  console.log(options) // => { name: 'vercel', Path: '/test' }
 
  // deleting a cookie will mark it as expired
  response.cookies.delete('vercel')
 
  return response
}

新的用户代理助手

更改摘要

  • 请求对象上不再可访问用户代理
  • 我们添加了一个新的userAgent助手,以将中间件大小减少17kb

说明

为了帮助减小中间件的大小,我们从请求对象中提取了用户代理并创建了一个新的助手userAgent

该助手从next/server导入,并允许您选择使用用户代理。该助手为您提供了与从请求对象中获得的相同属性。

如何升级

  • next/server导入userAgent助手
  • 解构您需要使用的属性

之前

pages/_middleware.ts
import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(request: NextRequest) {
  const url = request.nextUrl
  const viewport = request.ua.device.type === 'mobile' ? 'mobile' : 'desktop'
  url.searchParams.set('viewport', viewport)
  return NextResponse.rewrite(url)
}

之后

middleware.ts
import { NextRequest, NextResponse, userAgent } from 'next/server'
 
export function middleware(request: NextRequest) {
  const url = request.nextUrl
  const { device } = userAgent(request)
  const viewport = device.type === 'mobile' ? 'mobile' : 'desktop'
  url.searchParams.set('viewport', viewport)
  return NextResponse.rewrite(url)
}

不再有页面匹配数据

更改摘要

  • 使用URLPattern检查中间件是否正在为某个页面匹配调用

说明

目前,中间件根据 Next.js 路由清单(内部配置)估算您是否正在提供页面的资源。此值通过request.page显示。

为了使页面和资源匹配更准确,我们现在使用 Web 标准URLPattern API。

如何升级

使用URLPattern检查中间件是否正在为某个页面匹配调用。

之前

pages/_middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest, NextFetchEvent } from 'next/server'
 
export function middleware(request: NextRequest, event: NextFetchEvent) {
  const { params } = event.request.page
  const { locale, slug } = params
 
  if (locale && slug) {
    const { search, protocol, host } = request.nextUrl
    const url = new URL(`${protocol}//${locale}.${host}/${slug}${search}`)
    return NextResponse.redirect(url)
  }
}

之后

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
const PATTERNS = [
  [
    new URLPattern({ pathname: '/:locale/:slug' }),
    ({ pathname }) => pathname.groups,
  ],
]
 
const params = (url) => {
  const input = url.split('?')[0]
  let result = {}
 
  for (const [pattern, handler] of PATTERNS) {
    const patternResult = pattern.exec(input)
    if (patternResult !== null && 'pathname' in patternResult) {
      result = handler(patternResult)
      break
    }
  }
  return result
}
 
export function middleware(request: NextRequest) {
  const { locale, slug } = params(request.url)
 
  if (locale && slug) {
    const { search, protocol, host } = request.nextUrl
    const url = new URL(`${protocol}//${locale}.${host}/${slug}${search}`)
    return NextResponse.redirect(url)
  }
}

在内部 Next.js 请求上执行中间件

更改摘要

  • 中间件将为所有请求执行,包括_next

说明

在 Next.js v12.2 之前,中间件不会为_next请求执行。

对于使用中间件进行授权的情况,您应该迁移到使用rewrite/redirect到显示授权错误、登录表单或API路由的页面。

请参阅无响应主体,了解如何迁移到使用rewrite/redirect的示例。