前言
中间件(Middleware),一个听起来就很高级、很强大的功能。实际上也确实如此。使用中间件,你可以拦截并控制应用里的所有请求和响应。
比如你可以基于传入的请求,重写、重定向、修改请求或响应头、甚至直接响应内容。一个比较常见的应用就是鉴权,在打开页面渲染具体的内容前,先判断用户是否登录,如果未登录,则跳转到登录页面。
定义
写中间件,你需要在项目的根目录定义一个名为 middleware.js的文件:
// middleware.jsimport{NextResponse}from'next/server'// 中间件可以是 async 函数,如果使用了 awaitexportfunctionmiddleware(request){returnNextResponse.redirect(newURL('/home',request.url))}// 设置匹配路径exportconstconfig={matcher: '/about/:path*',}注意:这里说的项目根目录指的是和 pages 或 app 同级。但如果项目用了 src目录,则放在 src下。
在这个例子中,我们通过 config.matcher设置中间件生效的路径,在 middleware函数中设置中间件的逻辑,作用是将 /about、/about/xxx、/about/xxx/xxx 这样的的地址统一重定向到 /home。
设置匹配路径
了解了大致用途,现在让我们看下具体用法。
先说说如何设置匹配路径。有两种方式可以指定中间件匹配的路径。
matcher 配置项
第一种是使用 matcher配置项,示例代码如下:
exportconstconfig={matcher: '/about/:path*',}matcher 不仅支持字符串形式,也支持数组形式,用于匹配多个路径:
exportconstconfig={matcher: ['/about/:path*','/dashboard/:path*'],}初次接触的同学可能会对 :path* 这样的用法感到奇怪,这个用法来自于 path-to-regexp 这个库,它的作用就是将 /user/:name这样的路径字符串转换为正则表达式。Next.js 背后用的正是 path-to-regexp 解析地址。作为一个有着十年历史的开源库,path-to-regexp 还被 express、react-router、vue-router 等多个知名库引用。所以不妨让我们多多了解一下。
path-to-regexp 通过在参数名前加一个冒号来定义命名参数(Named Parameters),matcher 支持命名参数,比如 /about/:path匹配 /about/a和 /about/b,但是不匹配 /about/a/c
注:实际测试的时候,/about/:path 并不能匹配 /about/xxx,只能匹配 /about,如果要匹配 /about/xxx,需要写成 /about/:path/
命名参数的默认匹配逻辑是 [^/]+,但你也可以在命名参数后加一个括号,在其中自定义命名参数的匹配逻辑,比如 /about/icon-:foo(\\d+).png 匹配 /about/icon-1.png,但不匹配 /about/icon-a.png。
命名参数可以使用修饰符,其中 * 表示 0 个或 1 个或多个,?表示 0 个或 1 个,+表示 1 个或多个,比如:
/about/:path* 匹配 /about、/about/xxx、/about/xxx/xxx/about/:path? 匹配 /about、/about/xxx/about/:path+ 匹配 /about/xxx、/about/xxx/xxx
也可以在圆括号中使用标准的正则表达式,比如/about/(.*) 等同于 /about/:path*,比如 /(about|settings) 匹配 /about 和 /settings,不匹配其他的地址。/user-(hello|world)匹配 /user-hello和 /user-world。
一个较为复杂和常用的例子是:
exportconstconfig={matcher: [/* * 匹配所有的路径除了以这些作为开头的: * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) */'/((?!api|_next/static|_next/image|favicon.ico).*)',],}除此之外,还要注意,路径必须以 /开头。matcher 的值必须是常量,这样可以在构建的时候被静态分析。使用变量之类的动态值会被忽略。
matcher 的强大可远不止正则表达式,matcher 还可以判断查询参数、cookies、headers:
exportconstconfig={matcher: [{source: '/api/*',has: [{type: 'header',key: 'Authorization',value: 'Bearer Token'},{type: 'query',key: 'userId',value: '123'},],missing: [{type: 'cookie',key: 'session',value: 'active'}],},],}在这个例子中,不仅匹配了路由地址,还要求 header 的 Authorization 必须是 Bearer Token,查询参数的 userId 为 123,且 cookie 里的 session 值不是 active。
条件语句
第二种方法是使用条件语句:
import{NextResponse}from'next/server'exportfunctionmiddleware(request){if(request.nextUrl.pathname.startsWith('/about')){returnNextResponse.rewrite(newURL('/about-2',request.url))}if(request.nextUrl.pathname.startsWith('/dashboard')){returnNextResponse.rewrite(newURL('/dashboard/user',request.url))}}matcher 很强大,可有的时候不会写真的让人头疼,那就在具体的逻辑里写!
中间件逻辑
接下来我们看看中间件具体该怎么写:
exportfunctionmiddleware(request){// 如何读取和设置 cookies ?// 如何读取 headers ?// 如何直接响应?}如何读取和设置 cookies?
用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 cookies。
对于传入的请求,NextRequest 提供了 get、getAll、set和 delete方法处理 cookies,你也可以用 has检查 cookie 或者 clear删除所有的 cookies。
对于返回的响应,NextResponse 同样提供了 get、getAll、set和 delete方法处理 cookies。示例代码如下:
import{NextResponse}from'next/server'exportfunctionmiddleware(request){// 假设传入的请求 header 里 "Cookie:nextjs=fast"letcookie=request.cookies.get('nextjs')console.log(cookie)// => { name: 'nextjs', value: 'fast', Path: '/' }constallCookies=request.cookies.getAll()console.log(allCookies)// => [{ name: 'nextjs', value: 'fast' }]request.cookies.has('nextjs')// => truerequest.cookies.delete('nextjs')request.cookies.has('nextjs')// => false// 设置 cookiesconstresponse=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: '/' }// 响应 header 为 `Set-Cookie:vercel=fast;path=/test`returnresponse}在这个例子中,我们调用了 NextResponse.next() 这个方法,这个方法专门用在 middleware 中,毕竟我们写的是中间件,中间件进行一层处理后,返回的结果还要在下一个逻辑中继续使用,此时就需要返回 NextResponse.next()。当然如果不需要再走下一个逻辑了,可以直接返回一个 Response 实例,接下来的例子中会演示其写法。
如何读取和设置 headers?
用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 headers。示例代码如下:
// middleware.js import{NextResponse}from'next/server'exportfunctionmiddleware(request){// clone 请求标头constrequestHeaders=newHeaders(request.headers)requestHeaders.set('x-hello-from-middleware1','hello')// 你也可以在 NextResponse.rewrite 中设置请求标头constresponse=NextResponse.next({request: {// 设置新请求标头headers: requestHeaders,},})// 设置新响应标头 `x-hello-from-middleware2`response.headers.set('x-hello-from-middleware2','hello')returnresponse}这个例子比较特殊的地方在于调用 NextResponse.next 的时候传入了一个对象用于转发 headers,根据 NextResponse 的官方文档,目前也就这一种用法。
CORS
这是一个在实际开发中会用到的设置 CORS 的例子:
import{NextResponse}from'next/server'constallowedOrigins=['https://acme.com','https://my-app.org']constcorsOptions={'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS','Access-Control-Allow-Headers': 'Content-Type, Authorization',}exportfunctionmiddleware(request){// Check the origin from the requestconstorigin=request.headers.get('origin')??''constisAllowedOrigin=allowedOrigins.includes(origin)// Handle preflighted requestsconstisPreflight=request.method==='OPTIONS'if(isPreflight){constpreflightHeaders={
...(isAllowedOrigin&&{'Access-Control-Allow-Origin': origin}),
...corsOptions,}returnNextResponse.json({},{headers: preflightHeaders})}// Handle simple requestsconstresponse=NextResponse.next()if(isAllowedOrigin){response.headers.set('Access-Control-Allow-Origin',origin)}Object.entries(corsOptions).forEach(([key,value])=>{response.headers.set(key,value)})returnresponse}exportconstconfig={matcher: '/api/:path*',}如何直接响应?
用法跟路由处理程序一致,使用 NextResponse 设置返回的 Response。示例代码如下:
import{NextResponse}from'next/server'import{isAuthenticated}from'@lib/auth'exportconstconfig={matcher: '/api/:function*',}exportfunctionmiddleware(request){// 鉴权判断if(!isAuthenticated(request)){// 返回错误信息returnnewNextResponse(JSON.stringify({success: false,message: 'authentication failed'}),{status: 401,headers: {'content-type': 'application/json'}})}}执行顺序
在 Next.js 中,有很多地方都可以设置路由的响应,比如 next.config.js 中可以设置,中间件中可以设置,具体的路由中可以设置,所以要注意它们的执行顺序:
headers(next.config.js)redirects(next.config.js)- 中间件 (
rewrites, redirects 等) beforeFiles (next.config.js中的rewrites)- 基于文件系统的路由 (
public/, _next/static/, pages/, app/ 等) afterFiles (next.config.js中的rewrites)- 动态路由 (
/blog/[slug]) fallback中的 (next.config.js中的rewrites)
注: beforeFiles 顾名思义,在基于文件系统的路由之前,afterFiles顾名思义,在基于文件系统的路由之后,fallback顾名思义,垫底执行。
执行顺序具体是什么作用呢?其实我们写个 demo 测试一下就知道了,文件目录如下:
next-app ├─ app │ ├─ blog │ │ ├─ [id] │ │ │ └─ page.js │ │ ├─ louis │ │ │ └─ page.js │ │ └─ page.js ├─ middleware.js └─ next.config.js
next.config.js 中声明 redirects、rewrites:
module.exports={asyncredirects(){return[{source: '/blog/louis',destination: '/blog/louis_redirects',permanent: true,},]},asyncrewrites(){return{beforeFiles: [{source: '/blog/louis',destination: '/blog/louis_beforeFiles',},],afterFiles: [{source: '/blog/louis',destination: '/blog/louis_afterFiles',},],fallback: [{source: '/blog/louis',destination: `/blog/louis_fallback`,},],}},}middleware.js 的代码如下:
import{NextResponse}from'next/server'exportfunctionmiddleware(request){returnNextResponse.redirect(newURL('/blog/louis_middleware',request.url))}exportconstconfig={matcher: '/blog/louis',}app/blog/page.js代码如下:
import{redirect}from'next/navigation'exportdefaultfunctionPage(){redirect('/blog/louis_page')}app/blog/[id]/page.js代码如下:
import{redirect}from'next/navigation'exportdefaultfunctionPage(){redirect('/blog/louis_slug')}现在我们在多个地方都配置了重定向和重写,那么问题来了,现在访问 /blog/louis,最终浏览器地址栏里呈现的 URL 是什么?
答案是 /blog/louis_slug。按照执行顺序,访问 /blog/louis,先根据 next.config.js 的 redirects重定向到 /blog/louis_redirects,于是走到动态路由的逻辑,重定向到 /blog/louis_slug。
中间件相关配置项
Next.js v13.1 为中间件增加了两个新的配置项,skipMiddlewareUrlNormalize和skipTrailingSlashRedirect,用来处理一些特殊的情况。
skipTrailingSlashRedirect
首先解释一下 Trailing Slashes,中文翻译为“尾部斜杠”,它指的是放在 URL 末尾的正斜杠,举个例子: www.louisjs.com/users/地址中最后一个斜杠就是尾部斜杠。
一般来说,尾部斜杠用于区分目录还是文件,有尾部斜杠,表示目录,没有尾部斜杠,表示文件。当然这只是一个建议,具体你想怎么处理都行。
从 URL 的角度来看,www.louisjs.com/users/和 www.louisjs.com/users是两个地址,不过通常我们都会做重定向。比如你在 Next.js 中访问 /about/它会自动重定向到 /about,URL 也会变为 /about。
skipTrailingSlashRedirect 顾名思义,跳过尾部斜杠重定向,当你设置 skipTrailingSlashRedirect为 true 后,假设再次访问 /about/,URL 依然会是 /about/。
使用 skipTrailingSlashRedirect的示例代码如下:
// next.config.jsmodule.exports={skipTrailingSlashRedirect: true,}// middleware.jsconstlegacyPrefixes=['/docs','/blog']exportdefaultasyncfunctionmiddleware(req){const{ pathname }=req.nextUrlif(legacyPrefixes.some((prefix)=>pathname.startsWith(prefix))){returnNextResponse.next()}// 应用尾部斜杠if(!pathname.endsWith('/')&&!pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)){req.nextUrl.pathname+='/'returnNextResponse.redirect(req.nextUrl)}}在这个例子中,这里我们实现了除 /docs 和 /blog 作为前缀的路由之外,其他路由都自动添加上尾部斜杠。
skipMiddlewareUrlNormalize
关于 skipMiddlewareUrlNormalize,让我们直接看一个例子:
// next.config.jsmodule.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)
// 如果设置为 true,值为:/_next/data/build-id/hello.json
// 如果没有配置,值为: /hello
}
设置 skipMiddlewareUrlNormalize 为 true 后,可以获取路由原始的地址,常用于国际化场景中。
运行时
使用 Middleware 的时候还要注意一点,那就是目前 Middleware 只支持 Edge runtime,并不支持 Node.js runtime。这意味着写 Middleware 的时候,尽可能使用 Web API,避免使用 Node.js API
实战:控制请求数
需求:如果大家调用过 openai 的接口,常用的 ChatGPT v3.5 接口会有每分钟最多 3 次的调用限制。现在你也开发了一个 /api/chat 的接口,为了防止被恶意调用,限制每分钟最多调用 3 次。使用 Next.js 该怎么实现呢?
让我们来实现吧!为此我们需要安装一个依赖包 limiter:
新建 app/api/chat/route.js,代码如下:
import{NextResponse}from'next/server'import{RateLimiter}from"limiter";constlimiter=newRateLimiter({tokensPerInterval: 3,interval: "min",fireImmediately: true});exportasyncfunctionGET(){constremainingRequests=awaitlimiter.removeTokens(1);if(remainingRequests<0){returnnewNextResponse(JSON.stringify({success: false,message: 'Too Many Requests'}),{status: 429,headers: {'content-type': 'application/json'}})}returnNextResponse.json({data: "你好!"})}我们将控制次数的逻辑写在了具体的路由里,现在让我们尝试写在中间件里:
import{NextResponse}from'next/server'import{RateLimiter}from"limiter";constlimiter=newRateLimiter({tokensPerInterval: 3,interval: "min",fireImmediately: true});exportasyncfunctionmiddleware(request){constremainingRequests=awaitlimiter.removeTokens(1);if(remainingRequests<0){returnnewNextResponse(JSON.stringify({success: false,message: 'Too Many Requests'}),{status: 429,headers: {'content-type': 'application/json'}})}returnNextResponse.next()}// 设置匹配路径exportconstconfig={matcher: '/api/chat',}然而此时你会发现:

代码是报错的,这是为什么呢?
这就是初学者写中间件常犯的一个错误。之所以出错,是因为 limiter 其实是一个用在 node.js 环境的库,然而目前 Middleware 只支持 Edge runtime,并不支持 Node.js runtime,所以才会报错。举这个项目作为例子,只是为了提醒大家注意运行时问题。
中间件的代码维护
如果项目比较简单,中间件的代码通常不会写很多,将所有代码写在一起倒也不是什么太大问题。可当项目复杂了,比如在中间件里又要鉴权、又要控制请求、又要国际化等等,各种逻辑写在一起,中间件很快就变得难以维护。如果我们要在中间件里实现多个需求,该怎么合理的拆分代码呢?
一种简单的方式是:
import{NextResponse}from'next/server'asyncfunctionmiddleware1(request){console.log(request.url)returnNextResponse.next()}asyncfunctionmiddleware2(request){console.log(request.url)returnNextResponse.next()}exportasyncfunctionmiddleware(request){awaitmiddleware1(request)awaitmiddleware2(request)}exportconstconfig={matcher: '/api/:path*',}一种更为优雅的方式是借助高阶函数:
import{NextResponse}from'next/server'functionwithMiddleware1(middleware){returnasync(request)=>{console.log('middleware1 '+request.url)returnmiddleware(request)}}functionwithMiddleware2(middleware){returnasync(request)=>{console.log('middleware2 '+request.url)returnmiddleware(request)}}asyncfunctionmiddleware(request){console.log('middleware '+request.url)returnNextResponse.next()}exportdefaultwithMiddleware2(withMiddleware1(middleware))exportconstconfig={matcher: '/api/:path*',}请问此时的执行顺序是什么?试着打印一下吧。是不是感觉回到了学 redux 的时候?
但这样写起来还是有点麻烦,让我们写一个工具函数帮助我们:
import{NextResponse}from'next/server'functionchain(functions,index=0){constcurrent=functions[index];if(current){constnext=chain(functions,index+1);returncurrent(next);}return()=>NextResponse.next();}functionwithMiddleware1(middleware){returnasync(request)=>{console.log('middleware1 '+request.url)returnmiddleware(request)}}functionwithMiddleware2(middleware){returnasync(request)=>{console.log('middleware2 '+request.url)returnmiddleware(request)}}exportdefaultchain([withMiddleware1,withMiddleware2])exportconstconfig={matcher: '/api/:path*',}请问此时的执行顺序是什么?答案是按数组的顺序,middleware1、middleware2。
如果使用这种方式,实际开发的时候,代码类似于:
import{chain}from"@/lib/utils";import{withHeaders}from"@/middlewares/withHeaders";import{withLogging}from"@/middlewares/withLogging";exportdefaultchain([withLogging,withHeaders]);exportconstconfig={matcher: '/api/:path*',}具体写中间件时:
exportconstwithHeaders=(next)=>{returnasync(request)=>{// ...returnnext(request);};};参考链接
- nextjs.org/docs/app/bu…
- github.com/pillarjs/pa…
- nextjs.org/docs/messag…
- www.youtube.com/watch?v=fmF…
前言
中间件(Middleware),一个听起来就很高级、很强大的功能。实际上也确实如此。使用中间件,你可以拦截并控制应用里的所有请求和响应。
比如你可以基于传入的请求,重写、重定向、修改请求或响应头、甚至直接响应内容。一个比较常见的应用就是鉴权,在打开页面渲染具体的内容前,先判断用户是否登录,如果未登录,则跳转到登录页面。
定义
写中间件,你需要在项目的根目录定义一个名为
middleware.js的文件:注意:这里说的项目根目录指的是和
pages或app同级。但如果项目用了src目录,则放在src下。在这个例子中,我们通过
config.matcher设置中间件生效的路径,在middleware函数中设置中间件的逻辑,作用是将/about、/about/xxx、/about/xxx/xxx这样的的地址统一重定向到/home。设置匹配路径
了解了大致用途,现在让我们看下具体用法。
先说说如何设置匹配路径。有两种方式可以指定中间件匹配的路径。
matcher 配置项
第一种是使用
matcher配置项,示例代码如下:matcher不仅支持字符串形式,也支持数组形式,用于匹配多个路径:初次接触的同学可能会对
:path*这样的用法感到奇怪,这个用法来自于 path-to-regexp 这个库,它的作用就是将/user/:name这样的路径字符串转换为正则表达式。Next.js 背后用的正是 path-to-regexp 解析地址。作为一个有着十年历史的开源库,path-to-regexp 还被 express、react-router、vue-router 等多个知名库引用。所以不妨让我们多多了解一下。path-to-regexp 通过在参数名前加一个冒号来定义命名参数(Named Parameters),matcher 支持命名参数,比如
/about/:path匹配/about/a和/about/b,但是不匹配/about/a/c注:实际测试的时候,
/about/:path并不能匹配/about/xxx,只能匹配/about,如果要匹配/about/xxx,需要写成/about/:path/命名参数的默认匹配逻辑是
[^/]+,但你也可以在命名参数后加一个括号,在其中自定义命名参数的匹配逻辑,比如/about/icon-:foo(\\d+).png匹配/about/icon-1.png,但不匹配/about/icon-a.png。命名参数可以使用修饰符,其中
*表示 0 个或 1 个或多个,?表示 0 个或 1 个,+表示 1 个或多个,比如:/about/:path*匹配/about、/about/xxx、/about/xxx/xxx/about/:path?匹配/about、/about/xxx/about/:path+匹配/about/xxx、/about/xxx/xxx也可以在圆括号中使用标准的正则表达式,比如
/about/(.*)等同于/about/:path*,比如/(about|settings)匹配/about和/settings,不匹配其他的地址。/user-(hello|world)匹配/user-hello和/user-world。一个较为复杂和常用的例子是:
除此之外,还要注意,路径必须以
/开头。matcher的值必须是常量,这样可以在构建的时候被静态分析。使用变量之类的动态值会被忽略。matcher 的强大可远不止正则表达式,matcher 还可以判断查询参数、cookies、headers:
在这个例子中,不仅匹配了路由地址,还要求 header 的 Authorization 必须是 Bearer Token,查询参数的 userId 为 123,且 cookie 里的 session 值不是 active。
条件语句
第二种方法是使用条件语句:
matcher 很强大,可有的时候不会写真的让人头疼,那就在具体的逻辑里写!
中间件逻辑
接下来我们看看中间件具体该怎么写:
如何读取和设置 cookies?
用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 cookies。
对于传入的请求,NextRequest 提供了
get、getAll、set和delete方法处理 cookies,你也可以用has检查 cookie 或者clear删除所有的 cookies。对于返回的响应,NextResponse 同样提供了
get、getAll、set和delete方法处理 cookies。示例代码如下:在这个例子中,我们调用了
NextResponse.next()这个方法,这个方法专门用在 middleware 中,毕竟我们写的是中间件,中间件进行一层处理后,返回的结果还要在下一个逻辑中继续使用,此时就需要返回NextResponse.next()。当然如果不需要再走下一个逻辑了,可以直接返回一个 Response 实例,接下来的例子中会演示其写法。如何读取和设置 headers?
用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 headers。示例代码如下:
这个例子比较特殊的地方在于调用 NextResponse.next 的时候传入了一个对象用于转发 headers,根据 NextResponse 的官方文档,目前也就这一种用法。
CORS
这是一个在实际开发中会用到的设置 CORS 的例子:
如何直接响应?
用法跟路由处理程序一致,使用 NextResponse 设置返回的 Response。示例代码如下:
执行顺序
在 Next.js 中,有很多地方都可以设置路由的响应,比如 next.config.js 中可以设置,中间件中可以设置,具体的路由中可以设置,所以要注意它们的执行顺序:
headers(next.config.js)redirects(next.config.js)rewrites,redirects等)beforeFiles(next.config.js中的rewrites)public/,_next/static/,pages/,app/等)afterFiles(next.config.js中的rewrites)/blog/[slug])fallback中的 (next.config.js中的rewrites)注:
beforeFiles顾名思义,在基于文件系统的路由之前,afterFiles顾名思义,在基于文件系统的路由之后,fallback顾名思义,垫底执行。执行顺序具体是什么作用呢?其实我们写个 demo 测试一下就知道了,文件目录如下:
next.config.js中声明redirects、rewrites:middleware.js的代码如下:app/blog/page.js代码如下:app/blog/[id]/page.js代码如下:现在我们在多个地方都配置了重定向和重写,那么问题来了,现在访问
/blog/louis,最终浏览器地址栏里呈现的 URL 是什么?答案是
/blog/louis_slug。按照执行顺序,访问/blog/louis,先根据next.config.js的redirects重定向到/blog/louis_redirects,于是走到动态路由的逻辑,重定向到/blog/louis_slug。中间件相关配置项
Next.js v13.1 为中间件增加了两个新的配置项,
skipMiddlewareUrlNormalize和skipTrailingSlashRedirect,用来处理一些特殊的情况。skipTrailingSlashRedirect
首先解释一下 Trailing Slashes,中文翻译为“尾部斜杠”,它指的是放在 URL 末尾的正斜杠,举个例子:
www.louisjs.com/users/地址中最后一个斜杠就是尾部斜杠。一般来说,尾部斜杠用于区分目录还是文件,有尾部斜杠,表示目录,没有尾部斜杠,表示文件。当然这只是一个建议,具体你想怎么处理都行。
从 URL 的角度来看,
www.louisjs.com/users/和www.louisjs.com/users是两个地址,不过通常我们都会做重定向。比如你在 Next.js 中访问/about/它会自动重定向到/about,URL 也会变为/about。skipTrailingSlashRedirect 顾名思义,跳过尾部斜杠重定向,当你设置
skipTrailingSlashRedirect为 true 后,假设再次访问/about/,URL 依然会是/about/。使用
skipTrailingSlashRedirect的示例代码如下:在这个例子中,这里我们实现了除
/docs和/blog作为前缀的路由之外,其他路由都自动添加上尾部斜杠。skipMiddlewareUrlNormalize
关于 skipMiddlewareUrlNormalize,让我们直接看一个例子:
设置 skipMiddlewareUrlNormalize 为 true 后,可以获取路由原始的地址,常用于国际化场景中。
运行时
使用 Middleware 的时候还要注意一点,那就是目前 Middleware 只支持 Edge runtime,并不支持 Node.js runtime。这意味着写 Middleware 的时候,尽可能使用 Web API,避免使用 Node.js API
实战:控制请求数
需求:如果大家调用过 openai 的接口,常用的 ChatGPT v3.5 接口会有每分钟最多 3 次的调用限制。现在你也开发了一个
/api/chat的接口,为了防止被恶意调用,限制每分钟最多调用 3 次。使用 Next.js 该怎么实现呢?让我们来实现吧!为此我们需要安装一个依赖包 limiter:
新建
app/api/chat/route.js,代码如下:我们将控制次数的逻辑写在了具体的路由里,现在让我们尝试写在中间件里:
然而此时你会发现:
代码是报错的,这是为什么呢?
这就是初学者写中间件常犯的一个错误。之所以出错,是因为 limiter 其实是一个用在 node.js 环境的库,然而目前 Middleware 只支持 Edge runtime,并不支持 Node.js runtime,所以才会报错。举这个项目作为例子,只是为了提醒大家注意运行时问题。
中间件的代码维护
如果项目比较简单,中间件的代码通常不会写很多,将所有代码写在一起倒也不是什么太大问题。可当项目复杂了,比如在中间件里又要鉴权、又要控制请求、又要国际化等等,各种逻辑写在一起,中间件很快就变得难以维护。如果我们要在中间件里实现多个需求,该怎么合理的拆分代码呢?
一种简单的方式是:
一种更为优雅的方式是借助高阶函数:
请问此时的执行顺序是什么?试着打印一下吧。是不是感觉回到了学 redux 的时候?
但这样写起来还是有点麻烦,让我们写一个工具函数帮助我们:
请问此时的执行顺序是什么?答案是按数组的顺序,middleware1、middleware2。
如果使用这种方式,实际开发的时候,代码类似于:
具体写中间件时:
参考链接