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

496 阅读2分钟

今天分享一个来自社区的 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 中间件就实现了,可以让你的代码更好的维护,分离中间件的各个职责,祝大家用起来开心 ^_^