中间件升级指南
在我们努力改进中间件以实现通用可用性 (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
) 时,您需要遵循本升级指南来更新您的中间件。
重大更改
不支持嵌套中间件
更改摘要
- 在您的
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*
,自定义匹配器在导出的配置对象中定义
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*'],
}
匹配器配置还允许使用完整的正则表达式,因此支持诸如否定前瞻或字符匹配之类的匹配。此处显示了一个否定前瞻的示例,用于匹配除特定路径之外的所有路径
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).*)',
],
}
虽然配置选项是首选,因为它不会在每个请求上调用,但您也可以使用条件语句仅在匹配特定路径时运行中间件。使用条件语句的一个优点是定义中间件执行时的显式顺序。以下示例显示了如何合并两个以前嵌套的中间件
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
说明
为了尊重客户端和服务器端导航之间的差异,并帮助确保开发人员不会构建不安全的中间件,我们正在删除在中间件中发送响应主体的能力。这确保了中间件仅用于rewrite
、redirect
或修改传入请求(例如设置cookie)。
以下模式将不再有效
new Response('a text value')
new Response(streamOrBuffer)
new Response(JSON.stringify(obj), { headers: 'application/json' })
NextResponse.json()
如何升级
对于使用中间件进行响应(例如授权)的情况,您应该迁移到使用rewrite
/redirect
到显示授权错误、登录表单或API路由的页面。
之前
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 })
}
之后
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 路由
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.set | cookie |
cookies.delete | clearCookie |
cookies.getWithOptions | cookies |
说明
根据 Beta 反馈,我们正在更改NextRequest
和NextResponse
中的 Cookie API 以使其更符合get
/set
模型。Cookies
API 扩展了 Map,包括entries
和values
等方法。
如何升级
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
}
之后
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
助手 - 解构您需要使用的属性
之前
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)
}
之后
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
检查中间件是否正在为某个页面匹配调用。
之前
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)
}
}
之后
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
的示例。
这有帮助吗?