切換語言為:簡體

5分鐘 Nextjs 中介軟體快速優雅實現

  • 爱糖宝
  • 2024-11-05
  • 2030
  • 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.