切换语言为:繁体

5分钟 Nextjs 中间件快速优雅实现

  • 爱糖宝
  • 2024-11-05
  • 2029
  • 0
  • 0

今天分享一个来自社区的 nextjs 非常好的中间件实践, 我们知道 nextjs 原生提供的中间件比较原始简陋,如果想实现比较多的功能代码实现起来会比较丑陋

这个版本主要是让 nextj 支持类似 express 中间件的模型,可以使用链式调用中间件

  1. 我们在 src 或者 app 目录下新建 middlewares 文件夹,在 middlewares 文件夹下新建 util 文件夹

  2. util 文件夹下新建chain.ts

import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
import { mergeHeaders } from './merge-headers'

type GoNextMiddleware = () => 'continue'

export type MiddlewareFunction = (
  request: NextRequest,
  next: GoNextMiddleware,
  event: NextFetchEvent
) => Promise<NextResponse<unknown> | ReturnType<GoNextMiddleware>>

export function composeMiddleware(handlers: MiddlewareFunction[] = []) {
  const validMiddlewareHandlers = handlers.filter((handler) => typeof handler === 'function')

  return async function (request: NextRequest, event: NextFetchEvent) {
    const allResponses: NextResponse[] = []

    // 1.
    // run every middleware and collect responses (NextResponse)
    // until a middleware want to break the chain (redirect or rewrite)
    for (const fn of validMiddlewareHandlers) {
      const result = await fn(request, () => 'continue', event)

      // ensure that fn returned  something or notify the dev
      if (result !== 'continue' && !(result instanceof NextResponse)) {
        console.error(
          `The middleware chain has been broken because '${fn.name}' did not return a NextResponse or call next().`
        )

        return NextResponse.next()
      }

      // go next middleware
      if (result === 'continue') continue

      // we have a response
      allResponses.push(result)

      // the "middleware" function cannot return a native Response
      // @see https://nextjs.org/docs/messages/middleware-upgrade-guide#no-response-body
      // It can only :
      //   - return `NextResponse.redirect() or NextResponse.rewrite()`
      //        => this must break the chian
      //   - return a mutated request using `NextResponse.next({ request: { /* ... */ }})`
      //        => this must NOT break the chain
      const isRedirect = () => result.headers.get('Location')
      const isRewrite = () => result.headers.get('x-middleware-rewrite')
      if (isRedirect() || isRewrite()) {
        // break the chain
        break
      }
    }

    // 2.
    // return final response

    // middlewares have not returned any response, do nothing...
    if (allResponses.length === 0) return NextResponse.next()

    // only one middleware returned a response, return it
    if (allResponses.length === 1) return allResponses[0]

    // more than one middleware returned a response
    // merge headers into a final response and return it
    const finalResponse = allResponses[allResponses.length - 1]
    const finalHeaders: Headers = mergeHeaders(...allResponses.map((r) => r.headers))
    for (const [key] of Array.from(finalResponse.headers.entries())) {
      finalResponse.headers.delete(key)
    }
    for (const [key, value] of Array.from(finalHeaders.entries())) {
      finalResponse.headers.set(key, value)
    }
    return finalResponse
  }
}

  1. utils 文件夹下新建 merge-headers.ts

// @credits
// https://github.com/whitecrownclown/merge-headers/blob/master/index.ts

function isObject(value: any) {
  return value !== null && typeof value === 'object'
}

export function mergeHeaders(...sources: HeadersInit[]) {
  const result: Record<string, string> = {}

  for (const source of sources) {
    if (!isObject(source)) {
      throw new TypeError('All arguments must be of type object')
    }

    const headers: Headers = new Headers(source)

    for (const [key, value] of Array.from(headers.entries())) {
      if (value === undefined || value === 'undefined') {
        delete result[key]
      } else {
        result[key] = value
      }
    }
  }

  return new Headers(result)
}

  1. 下边我们举个 proxy 中间件的例子说明如何使用这一套链式中间件,在 middlewares 文件夹下新建 proxy.middleware.ts

import { NextResponse, type NextRequest } from 'next/server'

import { MiddlewareFunction } from '@/middlewares/utils/chain'

export const proxyMiddleware = (req: NextRequest) => {
  const destination = new URL(`${process.env.YOUR_BACKEND_SERVER}`)
  const url = req.nextUrl.clone()

  url.host = destination.host
  url.protocol = destination.protocol
  url.port = destination.port

  return NextResponse.rewrite(url)
}

export const handleProxyMiddleware: MiddlewareFunction = async (req, next) => {
  if (req.nextUrl.pathname.startsWith(`/api`)) {
    return proxyMiddleware(req)
  }

  return next()
}

这个是一个生产可用版本的 proxy middleware,用来代理访问后端接口,将 process.env.YOUR_BACKEND_SERVER 换成你自己的后端环境变量就OK了,我们可以看到这个 handleProxyMiddleware 包括了两个参数,一个是 next 包装后的 request 对象,另一个是 next() 方法,用来在执行结束后调用, 调用 next() 方法就会去到下一个中间件

  1. 接下来,在 middlewares 文件夹下新建 index.ts 文件夹,我们把这个 proxyMiddleware 放到这里来

import { handleProxyMiddleware } from './proxy.middleware'

export const middlewares = [handleProxyMiddleware]

OK,这时候我们主要的部分就结束了,你可以把新建的其余的middlewares加入到这个 middlewares 数组里面,需要注意的是,这里的数组顺序就是中间件执行的顺序,如果你需要有序的中间件,这里要注意放置的顺序

  1. 最后一步了,在 nextjs 约定的 src 或者 app 或者 pages 文件夹下新建 middleware.ts 文件放入如下代码:

import { composeMiddleware } from '@/middlewares/utils/chain'
import { middlewares } from './middlewares'

export default composeMiddleware(middlewares)

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)', '/api/:path*'],
}

到此为止,一个生产可用的,优雅的 nextjs 中间件就实现了,可以让你的代码更好的维护,分离中间件的各个职责,祝大家用起来开心 ^_^


作者:rickyshin93
链接:https://juejin.cn/post/7433615587262529573

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.