从0~1手写Nest.js - (6) 实现参数装饰器 @Request @Req

210 阅读3分钟

一. 前言

在前5篇文章中我们实现了以下几个必要的工作内容

  • 一个极简的Nest服务器的搭建
  • 封装Logger日志类 (项目运行时,实现控制台打印运行日志)
  • 自定义模块装饰器 (@Module) / 控制器装饰器 (@Controller) / 参数装饰器 (GET)
  • 配置路径别名 (主要用于切换node_modules中的@nestjs与自己写的@nestjs,方便切换看源码)
  • 实现项目热重载 (nodemon)
  • 配置路由 (项目启动时,映射全部路由)

二. 引出

我们目前虽然已经可以很好的实现根据切换路由从而获取对应方法返回的数据,但好像还是差了些什么...

例如:如何在请求接口时,获取当前请求的路径、请求方式、请求头等参数?

目前是无法实现的,但是可以参考Nestjs中的 @Requst@Req 这两个参数装饰器

@Request()装饰器主要用于控制器方法中,获取HTTP请求对象,从中访问请求头、请求体、查询参数、路径参数、认证信息等.

它为NestJS应用提供了灵活的方式来处理和解析客户端请求数据,适用于需要全面了解请求内容的场景.

知道了@Requsest@Req的作用,那么接下来就开始实现一下这两个装饰器叭!

三. 实现@Request与@Req 参数装饰器(前期准备工作)

  1. 首先在app.controller.ts同层级目录下新建一个user.controller.ts文件,用于测试@Requst@Req

结构如下:

image.png

  1. user.controller.ts中定义一个用于获取用户信息的测试接口

user.controller.ts

import { Controller, Get } from '@nestjs/common'

+ @Controller('users')
+ export class UserController {
+   @Get('/info')
+   userInfo() {
+     return 'getInfo'
+   }
+ }
  1. UserController挂载到AppModule用于映射路由

app.module.ts

// 从@nestjs/common模块中导入Module装饰器
import { Module } from '@nestjs/common'

// 从当前目录导入AppController控制器
import { AppController } from './app.controller'
import { UserController } from './user.controller'

// 使用@Module装饰器定义一个模块
@Module({
  // 在controllers属性中指定当前模块包含的控制器
+  controllers: [AppController, UserController]
})

// 定义并导出AppModule模块
export class AppModule {}

  1. @nestjs/common下新建param.decorator.ts文件,用于存放参数构造器@Request@Req

结构如下:

image.png

  1. 引入并导出param.decorator文件中的所有装饰器/方法

@nestjs/common/index.ts

export * from './module.decorator'
export * from './controller.decorator'
export * from './httpMethod.decorator'
+ export * from './param.decorator'

四. 实现@Request@Req(编码)

  1. 定义一个工厂函数,用于创建@Request或@Req装饰器

param.decorator.ts

// 引入元数据
import 'reflect-metadata'

// 创建参数装饰器
export const createParamDecorator = (key: string) => {
  /**
   * target: 控制器的原型
   * propertyKey: 方法名
   * parameterIndex:  例如userInfo的参数列表为(@Request() request: ExpressRequest, @Req() req: ExpressRequest)
   * @Request() request: ExpressRequest: 索引为0
   * @Req() req: ExpressRequest: 索引为1
   * 执行顺序是先走1再走0
   * @return 返回一个装饰器函数
   */

  return () => (target: any, propertyKey: string, parameterIndex: number) => {
    // 判断控制器类的原型,也就是getInfo方法属性上是否存在`params`这个元数据,如不存在则设置为空数组
    const existingParameters = Reflect.getMetadata(`params`, target, propertyKey) || []
    
    // 给控制器类的原型,也就是getInfo方法属性上添加元数据
    existingParameters.push({ parameterIndex, key }) // {parameterIndex: 索引, key: 'Request'或者'Req'}
    Reflect.defineMetadata(`params`, existingParameters, target, propertyKey)
  }
}

export const Request = createParamDecorator('Request')
export const Req = createParamDecorator('Req')
  1. user.controller.ts中使用@Request@Req

user.controller.ts

import { Controller, Get, Request, Req } from '@nestjs/common'
import type { Request as ExpressRequest } from 'express'

@Controller('users')
export class UserController {
  @Get('/info')
  userInfo(@Req() req: ExpressRequest, @Request() request: ExpressRequest) {
    console.log('req.path:', req.path)
    console.log('req.method:', req.method)
    console.log('req.url:', req.url)
    console.log('request.path:', request.path)
    console.log('request.method:', request.method)
    console.log('request.url:', request.url)
    return 'getInfo'
  }
}
  1. 项目初始化时,映射路由并解析参数

@nestjs/core/nestApplication.ts

// 导入元数据包
import 'reflect-metadata'
import express from 'express'
import type {
  Express,
  Request as ExpressRequest,
  Response as ExpressResponse,
  NextFunction as ExpressNextFunction
} from 'express'
import { Logger } from './logger'
import path from 'path'

interface paramMetaDataType {
  key: string
  parameterIndex: number
}

export class NestApplication {
  // 在内部私有化一个express实例
  private readonly app: Express = express()

  // protected readonly module等同于 this.module = module
  constructor(protected readonly module) {}

  // 初始化配置
  async init() {
    // 获取模块中所有的控制器类,准备做路由映射
    const controllers = Reflect.getMetadata('controllers', this.module)
    console.log('controllers:', controllers)

    // 打印执行日志
    Logger.log(`${this.module.name} dependencies initialized`, 'InstanceLoader')

    // 路由映射的核心是要知道,什么样的请求方法,什么样的请求路径,请求是对应的那个处理函数
    for (const Controller of controllers) {
      // 创建每个控制器的实例
      const controllerInstance = new Controller()
      console.log('controllerInstance:', controllerInstance)

      // 获取每个控制器的路径前缀
      const prefix = Reflect.getMetadata('prefix', Controller) || '/'

      // 打印执行日志: 提示开始路由解析
      Logger.log(`${Controller.name} {${prefix}}`, 'RoutesResolver')

      // 获取每个控制器类的原型
      const controllerPrototype = Controller.prototype
      console.log('controllerPrototype:', controllerPrototype)

      // 遍历控制器类原型上的方法名
      for (const methodName of Object.getOwnPropertyNames(controllerPrototype)) {
        // 根据方法名获取该控制器类原型上的方法
        const method = controllerPrototype[methodName]

        // 获取该方法上的元数据
        const httpMethod = Reflect.getMetadata('method', method)
        const pathMetadata = Reflect.getMetadata('path', method)

        // 如果方法名不存在(没有写GET/POST...请求装饰器的),则不处理
        if (!httpMethod) continue

        // 拼出完整的路由路径
        const routePath = path.posix.join('/', prefix, pathMetadata)

        // 配置路由,当客户端以httpMethod方法请求routePath路径的时候,会由对应的函数进行处理
        this.app[httpMethod.toLowerCase()](
          routePath,
          (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => {
            console.log('controllerInstance:', controllerInstance)
            console.log('methodName', methodName)

          +  const args = this.resolveParms(controllerInstance, methodName, req, res, next)
          +  console.log('args:', args)

            const result = method.call(controllerInstance, ...args)
            res.send(result)
          }
        )
        Logger.log(`Mapped {${routePath}, ${httpMethod}} route`, 'RoutesResolver')
      }
    }
  }

  /**
   * 该方法解析控制器方法的参数,将请求中的数据与控制器方法参数对应起来。它通过元数据获取参数的顺序,并根据装饰器的顺序为参数提供正确的值。
   * @param controllerInstance 控制器实例
   * @param methodName 方法名
   * @param req
   * @param res
   * @param next
   */
  + private resolveParms(
    controllerInstance: any,
    methodName: string,
    req: ExpressRequest,
    res: ExpressResponse,
    next: ExpressNextFunction
  ) {
    /**
     * 1.获取参数的元数据
     * 2.根据parameterIndex对数组排序:
     * 例如Controller中参数装饰器的顺序为: userInfo(@Req() req: ExpressRequest, @Request() request: ExpressRequest)
     * 因为在解析装饰器时会先走索引为1的装饰器,所以paramMetaData得到的结果是为[{ parameterIndex: 0, key: 'Request' },{ parameterIndex: 1, key: 'Req' }]
     * 所以需要对元数据进行排序从而对参数装饰器的索引顺序一一对应
     */
    const paramsMetaData: paramMetaDataType[] = Reflect.getMetadata(
      'params',
      controllerInstance,
      methodName
    )

    const sortafterParamsArr = paramsMetaData.sort((a, b) => a.parameterIndex - b.parameterIndex)
    console.log('requestArr:', sortafterParamsArr)

    const paramsArr = sortafterParamsArr.map((paramMetadata) => {
      const { key } = paramMetadata

      switch (key) {
        case 'Request':
        case 'Req':
          return req
        default:
          return null
      }
    })

    return paramsArr
  }

  // 启动HTTP服务器
  async listen(port: number) {
    this.init()

    // 调用express实例的listen方法,启动一个HTTP服务器,监听port端口
    this.app.listen(port, () => {
      Logger.log(`Application is running on http://localhost:${port}`, 'NestApplication')
    })
  }
}

五. 成果

访问users/info,检查是否能正常返回数据('getInfo')并查看控制台是否打印出了请求信息

浏览器

image.png

控制台

image.png

本章完结~