中间件升级指南
在我们努力改进中间件以使其正式发布 (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) 时,您需要遵循本升级指南来更新您的中间件。
重大更改
无嵌套中间件
更改摘要
- 在您的
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',
})
}Cookies API 改进
更改摘要
| 已添加 | 已移除 |
|---|---|
cookies.set | cookie |
cookies.delete | clearCookie |
cookies.getWithOptions | cookies |
说明
根据测试版反馈,我们正在更改 NextRequest 和 NextResponse 中的 Cookies API,使其更符合 get/set 模型。Cookies API 扩展了 Map,包括 entries 和 values 等方法。
如何升级
NextResponse 现在有一个 cookies 实例,其中包含
cookies.deletecookies.setcookies.getWithOptions
以及 Map 的其他扩展方法。
之前
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
}新增 User-Agent 助手
更改摘要
- User agent 不再在请求对象上可用
- 我们添加了一个新的
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 的示例。
这有帮助吗?