Next.js手把手系列:06、路由篇 | 中间件

  • Louis
  • 更新于 2024-07-30 21:42
  • 阅读 397

前言中间件(Middleware),一个听起来就很高级、很强大的功能。实际上也确实如此。使用中间件,你可以拦截并控制应用里的所有请求和响应。比如你可以基于传入的请求,重写、重定向、修改请求或响应头、甚至直接响应内容。

前言

中间件(Middleware),一个听起来就很高级、很强大的功能。实际上也确实如此。使用中间件,你可以拦截并控制应用里的所有请求和响应。

比如你可以基于传入的请求,重写、重定向、修改请求或响应头、甚至直接响应内容。一个比较常见的应用就是鉴权,在打开页面渲染具体的内容前,先判断用户是否登录,如果未登录,则跳转到登录页面。

定义

写中间件,你需要在项目的根目录定义一个名为 middleware.js的文件:

// middleware.js
import { NextResponse } from 'next/server'

// 中间件可以是 async 函数,如果使用了 await
export function middleware(request) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// 设置匹配路径
export const config = {
  matcher: '/about/:path*',
}

注意:这里说的项目根目录指的是和 pages 或 app 同级。但如果项目用了 src目录,则放在 src下。

在这个例子中,我们通过 config.matcher设置中间件生效的路径,在 middleware函数中设置中间件的逻辑,作用是将 /about/about/xxx/about/xxx/xxx 这样的的地址统一重定向到 /home

设置匹配路径

了解了大致用途,现在让我们看下具体用法。

先说说如何设置匹配路径。有两种方式可以指定中间件匹配的路径。

matcher 配置项

第一种是使用 matcher配置项,示例代码如下:

export const config = {
  matcher: '/about/:path*',
}

matcher 不仅支持字符串形式,也支持数组形式,用于匹配多个路径:

export const config = {
  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

一个较为复杂和常用的例子是:

export const config = {
  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:

export const config = {
  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'

export function middleware(request) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

matcher 很强大,可有的时候不会写真的让人头疼,那就在具体的逻辑里写!

中间件逻辑

接下来我们看看中间件具体该怎么写:

export function middleware(request) {
  // 如何读取和设置 cookies ?
  // 如何读取 headers ?
  // 如何直接响应?
}

如何读取和设置 cookies?

用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 cookies。

对于传入的请求,NextRequest 提供了 getgetAllset和 delete方法处理 cookies,你也可以用 has检查 cookie 或者 clear删除所有的 cookies。

对于返回的响应,NextResponse 同样提供了 getgetAllset和 delete方法处理 cookies。示例代码如下:

import { NextResponse } from 'next/server'

export function middleware(request) {
  // 假设传入的请求 header 里 "Cookie:nextjs=fast"
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]

  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false

  // 设置 cookies
  const response = 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`
  return response
}

在这个例子中,我们调用了 NextResponse.next() 这个方法,这个方法专门用在 middleware 中,毕竟我们写的是中间件,中间件进行一层处理后,返回的结果还要在下一个逻辑中继续使用,此时就需要返回 NextResponse.next()。当然如果不需要再走下一个逻辑了,可以直接返回一个 Response 实例,接下来的例子中会演示其写法。

如何读取和设置 headers?

用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 headers。示例代码如下:

// middleware.js 
import { NextResponse } from 'next/server'

export function middleware(request) {
  //  clone 请求标头
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')

  // 你也可以在 NextResponse.rewrite 中设置请求标头
  const response = NextResponse.next({
    request: {
      // 设置新请求标头
      headers: requestHeaders,
    },
  })

  // 设置新响应标头 `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

这个例子比较特殊的地方在于调用 NextResponse.next 的时候传入了一个对象用于转发 headers,根据 NextResponse 的官方文档,目前也就这一种用法。

CORS

这是一个在实际开发中会用到的设置 CORS 的例子:


import { NextResponse } from 'next/server'

const allowedOrigins = ['https://acme.com', 'https://my-app.org']

const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

export function middleware(request) {
  // Check the origin from the request
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)

  // Handle preflighted requests
  const isPreflight = request.method === 'OPTIONS'

  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }

  // Handle simple requests
  const response = NextResponse.next()

  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }

  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value)
  })

  return response
}

export const config = {
  matcher: '/api/:path*',
}
``...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
0x2b75...1A7D
区块链开发工程师,技术交流或者有工作机会可加VX: magicalLouis