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

中间件升级指南

在我们努力改进中间件以使其正式发布 (GA) 的过程中,我们根据您的反馈对中间件 API(以及您如何在应用程序中定义中间件)进行了一些更改。

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

  • 当前使用测试版 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. Cookie API 改进
  4. 新增 User-Agent 助手
  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',
  })
}

Cookies API 改进

更改摘要

已添加已移除
cookies.setcookie
cookies.deleteclearCookie
cookies.getWithOptionscookies

说明

根据测试版反馈,我们正在更改 NextRequestNextResponse 中的 Cookies 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
}

新增 User-Agent 助手

更改摘要

  • User agent 不再在请求对象上可用
  • 我们添加了一个新的 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 的示例。