从零开始实现一个 NestJS - 中间件

本系列的相关代码存放于 InkSha/expressive: 一个简易的仿造 Nest.js 的 NodeJS 后台框架。

本节,我们将实现中间件功能。

以下是一个用于打印请求的中间件例子。

import { Middleware } from '@expressive/common'
import { Request, Response, NextFunction } from 'express'

export class Logger extends Middleware {
  constructor() {
    super()
    console.log('logger init')
  }

  // 每次发生请求时
  // 都会打印请求的方法,路由 和 请求时间
  public use(req: Request, res: Response, next: NextFunction): void {
    const info = [
      `[${req.method}]`,
      `[${req.url}]`,
      new Date().toLocaleString()
    ].join(' ')

    console.log(info)

    next()
  }
}

定义基类

定义一个抽象类 Middleware ,后续所有的中间件都将基于它。

import type { NextFunction, Request, Response } from 'express'

export abstract class Middleware {
  public abstract use(req: Request, res: Response, next: NextFunction): void
}

定义装饰器

我们希望中间件可以应用在控制器和具体路由方法中。

@UseMiddleware(Logger)
@Controller("user")
export class UserController {
  @UseMiddleware(Auth)
  getUserInfo(){
    // ...
  }
}

因此我们需要增加一个装饰器。

// UseMiddleware 将可以被应用到类或方法上
// 因此返回的将是类装饰器和方法装饰器的联合类型
// 此外,因为可能会需要添加多个中间件
// 因此这里使用剩余参数运算符将参数变成一个数组
export type UseMiddleware = (...middlewares: Constructor<Middleware>[]) => ClassDecorator & MethodDecorator

export const UseMiddleware: UseMiddleware = (...middlewares) => (target: Object, property?: string | symbol) => {
  const old = Reflect.getMetadata(TokenConfig.ModuleMiddleware, target, property) || []
  // 当作用在类上面时
  // 将作为类装饰器被调用
  // 导致 property 为 undefined
  // 简单来说就是 property 未定义就当做没有这个参数了
  // 因此 Reflect.defineMetadata(key, val, target, property)
  // 等于 Reflect.defineMetadata(key, val, target)
  Reflect.defineMetadata(TokenConfig.ModuleMiddleware, [].concat(old, middlewares), target, property)
}

取出中间件

这一步将修改我们的 Router 类。

Router.parseRouterFnData 中添加取出中间件的逻辑并返回。

// ...
const middlewares = Reflect.getMetadata(TokenConfig.ModuleMiddleware, entity, name) as Array<Constructor<Middleware>>

return { fn, url, ..., middlewares }
// ...

接着在 Router.bindRouter 中取出定义在控制器类上的中间件,它们将作用在这个控制器包含的所有路由。

public bindRouter(entity: Object) {
  const baseUrl = Reflect.getMetadata(TokenConfig.Controller, this.controller)
  const entityMethodNames = this.getMethodList(this.controller)
  // 取出公共中间件
  const baseMiddlewares = (Reflect.getMetadata(TokenConfig.ModuleMiddleware, this.controller) || []) as Array<Constructor<Middleware>>

  for (const name of entityMethodNames) {
    const { fn, url, method, params, statusCode, middlewares: routerMiddlewares = [] } = this.parseRouterFnData(entity, name, baseUrl)

    this.router[HttpRequestName[method]](url,
      // 应用中间件
      baseMiddlewares
        .concat(routerMiddlewares)
        // 每次请求都会重新创建一个中间件的实例
        .map(Middleware => new Middleware().use),
      async (req: Request, res: Response, next: NextFunction) => {
        if (statusCode) res.status(statusCode)
        this.callHandle(() => {
          const p = this.getParams(req, res, next, params)
          return fn.apply(entity, p)
        }).then((data) => {
          res.send(data)
        })
      }
    )
  }

  return this.router
}

编写中间件

以下是两个简单的中间件示例。

export class Auth extends Middleware {
  public use(req: Request, res: Response, next: NextFunction): void {
    console.log('need auth')

    if (req.headers) {
      console.log({ token: req.headers.token })
    }

    next()
  }
}

export class Logger extends Middleware {
  constructor() {
    super()
    console.log('logger init')
  }

  public use(req: Request, res: Response, next: NextFunction): void {
    const info = [
      `[${req.method}]`,
      `[${req.url}]`,
      new Date().toLocaleString()
    ].join(' ')

    console.log(info)

    next()
  }
}

接着应用中间件。

@UseMiddleware(Logger)
@Controller("user")
export class UserController {
  constructor(
    private readonly service: UserService,
    private readonly articles: ArticleService,
    private readonly config: ConfigService,
  ) {}

  // ...

  @Get("articles")
  @UseMiddleware(Auth)
  public async getArticles(@Query("name") name: string) {
    return {
      name,
      count: this.articles.count,
      articles: this.articles.countArticles(name),
    }
  }

  // ...
}

# 输出
[GET] [/user/login?name=123] 2024/11/26 15:03:45
[GET] [/user/login?name=123] 2024/11/26 15:03:46
[GET] [/user/login?name=123] 2024/11/26 15:03:46
[GET] [/user/login?name=123] 2024/11/26 15:03:47
[GET] [/user/login?name=123] 2024/11/26 15:03:47
[GET] [/user/version] 2024/11/26 15:03:52
[GET] [/user/articles] 2024/11/26 15:03:55
need auth
{ token: undefined }

从零开始实现一个 NestJS - 中间件
http://www.inksha.com/archives/cong-ling-kai-shi-shi-xian-yi-ge-nestjs---zhong-jian-jian
作者
inksha
发布于
2025年02月27日
许可协议