网站搜索

typescript - 类装饰器 Nestjs 修改类中的每个方法


我想创建一个装饰器,它可以获取一个类的所有方法并用某种功能包裹它们,对于这个例子来说,只是像这样进行日志记录:

export function CustDec<T extends new (...args: any[]) => any>(Target: T) {
  return class extends Target {
    constructor(...args: any[]) {
      super(...args);
      console.log('@--------------------@');
      (Target as any).prototype.alphaMethod = async (args: any[]) => {
        console.log('@-before-@');
        await (Target as any).alphaMethod();
        console.log('@-after-@');
      };
    }
  };
}

问题出在我将这个装饰器应用到我的类上时:

import { Controller, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Firewall } from 'src/auth/decorators/firewall.decorator';
import { CustDec } from './feedback.decorator';

@CustDec
@ApiTags('feedback')
@Controller('feedback')
export class FeedbackController {
  constructor() {
    setTimeout(async () => {
      await this.alphaMethod(); // <--- here
    }, 3000);
  }

  @Firewall()
  @Post('')
  async alphaMethod() { // <-- and here
    return 'some promised result';
  }
}

当我尝试在控制器上调用 /feedback 端点时,我遇到了一个错误,就好像 Firewall 和 Post 装饰器在我的 CustDec 修改了 alphaMethod 后失效了。即使我在 FeedbackController 构造函数中调用 alphaMethod,我也会得到另一个错误:

@-before-@
This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason:
TypeError: Target.alphaMethod is not a function
    at FeedbackController.Target.alphaMethod (/home/zagrava/workspace/test-nest/backend/src/feedback/feedback.decorator.ts:8:31)
    at Timeout._onTimeout (/home/zagrava/workspace/test-nest/backend/src/feedback/feedback.controller.ts:12:18)
    at listOnTimeout (node:internal/timers:564:17)
    at processTimers (node:internal/timers:507:7)

我如何让我的 CustDec 正确地为一个类的所有方法添加日志包装?

答案#1

类装饰器可以像下面这样实现:

function intercept<T extends { new(...args: any[]): {} }>(target: T) {
  const methods = getMethods(target.prototype);
  for (const method of methods) {
    const currentMethod = target.prototype[method]
    target.prototype[method] = async (...args: any[]) => {
      console.log("intercepted", new Date());
      const result = currentMethod(args)
      if (result instanceof Promise) {
        await result
      }
      console.log("executed", new Date());
      return result;
    }
  }
}

这个想法是获取类的所有函数(除了构造函数本身)并将它们封装在你自定义的函数中。像下面这样获取类函数的辅助方法在这种情况下非常有用:

const getMethods = (obj: any) => Object.getOwnPropertyNames(obj).filter(item => typeof obj[item] === 'function' && item !== "constructor")

操场

多个装饰器的作用

答案#2

像 POST 和 Firewall 这样的装饰器使用 Reflect 包创建元数据,所以只需将原始方法的元数据复制粘贴到目标方法上,就可以了。

function copyMetadata(
  originalMethod: any,
  targetMethod: any,
) {
  const metadataKeys = Reflect.getOwnMetadataKeys(originalMethod);
  for (const key of metadataKeys) {
    const metadata = Reflect.getMetadata(key, originalMethod);
    Reflect.defineMetadata(key, metadata, targetMethod);
  }
}