初探typescript装饰器在一些场景中的应用

先从一个简单的参数装饰器入手来了解下装饰器是怎么用的?

如何用参数装饰器进行参数验证?

参数装饰器是一个函数,它接收以下参数:

  • 类的原型(如果是静态方法,则是类的构造函数)。
  • 方法名称。
  • 参数在函数参数列表中的索引。
js 复制代码
function ParameterDecorator(target: any, methodName: string, parameterIndex: number) {
  console.log("Parameter Decorator called");
  console.log("Target:", target);
  console.log("Method Name:", methodName);
  console.log("Parameter Index:", parameterIndex);
}

class MyClass {
  myMethod(@ParameterDecorator arg1: string, arg2: number) {
    console.log(`Executing myMethod with args: ${arg1}, ${arg2}`);
  }
}

const instance = new MyClass();
instance.myMethod("Hello", 42);

那它有什么用呢?可以用于实现参数的验证,你可以在方法执行前对参数进行验证。

以下是一个完整的示例,展示如何使用参数装饰器实现参数验证:

js 复制代码
import 'reflect-metadata';

# 定义验证规则接口
interface ValidationRule {
  validate(value: any): boolean;
  message: string;
}

# 定义具体的验证规则
class IsNumber implements ValidationRule {
  validate(value: any): boolean {
    return typeof value === 'number';
  }
  message = '参数必须是数字';
}

class IsString implements ValidationRule {
  validate(value: any): boolean {
    return typeof value === 'string';
  }
  message = '参数必须是字符串';
}

# 参数装饰器工厂函数
function Validate(rule: ValidationRule) {
  return function (target: any, methodName: string, parameterIndex: number) {
    // 获取已有的验证规则
    const rules = Reflect.getMetadata('validationRules', target, methodName) || [];
    // 添加新的验证规则
    rules.push({ index: parameterIndex, rule });
    // 存储验证规则
    Reflect.defineMetadata('validationRules', rules, target, methodName);
  };
}

# 方法装饰器,用于拦截方法调用并验证参数
function ValidateParameters(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    // 获取验证规则
    const rules = Reflect.getMetadata('validationRules', target, methodName) || [];

    // 验证每个参数
    for (const { index, rule } of rules) {
      if (!rule.validate(args[index])) {
        throw new Error(`参数 ${index} 验证失败: ${rule.message}`);
      }
    }

    // 如果验证通过,调用原始方法
    return originalMethod.apply(this, args);
  };
}

// 示例类
class Example {
  @ValidateParameters
  greet(@Validate(new IsString()) name: string, @Validate(new IsNumber()) age: number) {
    console.log(`Hello, ${name}! You are ${age} years old.`);
  }
}

// 测试
const example = new Example();

example.greet('Alice', 30); // 正常执行
example.greet('Bob', '30'); // 抛出错误:参数 1 验证失败: 参数必须是数字
example.greet(123, 30); // 抛出错误:参数 0 验证失败: 参数必须是字符串

验证规则:

定义了 ValidationRule 接口和具体的验证规则类(如 IsNumber 和 IsString)。每个规则类实现了 validate 方法和 message 属性。

参数装饰器:

Validate 是一个参数装饰器工厂函数,用于将验证规则与参数关联。使用 Reflect.defineMetadata 将验证规则存储到元数据中。

方法装饰器:

ValidateParameters 是一个方法装饰器,用于拦截方法调用。在方法执行前,读取元数据中的验证规则,并验证参数。如果验证失败,抛出错误;否则,调用原始方法。

如何用参数装饰器进行依赖注入?

js 复制代码
import "reflect-metadata";

# 定义参数装饰器
function Inject(token: string) {
  return function (target: any, methodName: string, parameterIndex: number) {
    Reflect.defineMetadata("inject", token, target, `${methodName}_${parameterIndex}`);
  };
}

# 定义服务类
class MyService {
  doSomething() {
    console.log("MyService is doing something");
  }
}

# 定义需要注入的类
class MyClass {
  # 使用装饰器Inject注入MyService
  constructor(@Inject("MyService") private myService: MyService) {}

  doSomething() {
    this.myService.doSomething();
  }
}

# 定义依赖注入容器
class Container {
  private instances = new Map<string, any>();
    
  # 注册
  register(token: string, instance: any) {
    this.instances.set(token, instance);
  }

  resolve<T>(token: string): T {
    const instance = this.instances.get(token);
    if (!instance) {
      throw new Error(`No instance found for token: ${token}`);
    }
    return instance;
  }
  
  # 依赖注入
  inject<T>(target: any): T {
    const instance = new target();
    
    # 在 TypeScript 里,`design:paramtypes` 是一种由 TypeScript 编译器自动生成的元数据,
    # 借助反射机制,它能让我们在运行时获取构造函数参数的类型。
    # 这里会返回构造函数参数类型数组,即[MyService]
    
    # [MyService]
    const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
    for (let i = 0; i < paramTypes.length; i++) {
      # 获取 Reflect.defineMetadata注入的元数据
      const token = Reflect.getMetadata("inject", target.prototype, `constructor_${i}`);
      
      if (token) {
        # 获取MyService实例
        const dependency = this.resolve(token);
        
        # 把MyService实例赋值给MyClass的属性myService上,这样就完成了依赖注入
        instance[i] = dependency;
      }
    }

    return instance;
  }
}

# 使用依赖注入
const container = new Container();
container.register("MyService", new MyService());

# 通过container.inject实例化MyClass
const myClassInstance = container.inject(MyClass);
myClassInstance.doSomething(); // 输出: MyService is doing something

首先要知道,所有的类的实例化都是在容器中进行的(控制反转)。至于怎么实现的控制反转,这就是通过装饰器和元数据实现的。

【代码解释】

  • 在需要注入依赖的地方,通过参数装饰器注入元数据,这个元数据就是类的一个标志key(MyService),constructor(@Inject("MyService") private myService: MyService) {}

  • 通过容器注册实例,container.register("MyService", new MyService());

  • 在实例化MyClass时,就不是通过new的方式了,而是通过容器container.inject(MyClass),在这个方法中取出元数据,然后获取到元数据所对应类的实例,然后把实例赋值给MyClass。

利用装饰器改写express成nestjs的样子

我们先看看改写之后的样子:

js 复制代码
# 中间件:验证用户是否登录
const checkLogin = (req: Request, res: Response, next: NextFunction): void => {
  const isLogin = !!req.session?.isLogin
  if (isLogin) {
    next()
  } else {
    res.json(getResponseResult(null, 'please login'))
  }
}

# 定义父路由路径
@controller('/api')
export class CrowllerController {
  # 定义方法路由
  @get('/getData')
  # 注册中间件
  @use(checkLogin)
  getData(req: Request, res: Response): void {
    ...
  }

  @get('/showData')
  @use(checkLogin)
  showData(req: Response, res: Response): void {
    ...
  }
}

这就很像nestjs的代码了。那它是怎么实现的呢?

首先看看装饰器get post实现逻辑:

js 复制代码
# get post 装饰器

enum Methods {
  get = 'get',
  post = 'post'
}
function getRequestDecorator(type: Methods) {
  return function (path: string) {
    // target就是类的原型对象
    return function (target: LoginController, key: string) {
      Reflect.defineMetadata('path', path, target, key)
      Reflect.defineMetadata('method', type, target, key)
    }
  }
}
export const get = getRequestDecorator(Methods.get)
export const post = getRequestDecorator(Methods.post)

这段代码很简单,就是定义了两个get、post两个装饰器,在装饰器里面通过元数据Reflect.defineMetadata在LoginController的方法 login 和 logout 上添加了 path 和 method 两个元数据,例如,login方法上的元数据为:

js 复制代码
{
   path: '/login',
   method: 'post'
}

那什么时候获取到元数据呢?那就是类的装饰器controller

这里需要知道,方法的装饰器是先于类的装饰器之前执行,所以,能在类的装饰器上获取到在方法的装饰器上定义的元数据。

js 复制代码
# 定义类的装饰器controller

export function controller(root: string) {

  # target就是类的构造函数,通过target.prototype获取类的原型
  return function (target: new (...args: any[]) => any) {
    for (let key in target.prototype) {
      
      # 获取路由
      const path: string = Reflect.getMetadata('path', target.prototype, key)
      
      # 获取请求方法
      const method: Methods=Reflect.getMetadata('method',target.prototype,key)
      
      # 获取对应的处理函数
      const handle = target.prototype[key]
      
      # 获取中间件
      const middleware: RequestHandler = Reflect.getMetadata(
        'middleware',target.prototype,key)
      
      # 拼接路由
      if (path && method) {
        let fullpath = ''
        if (root === '/') {
          if (path === '/') {
            fullpath = '/'
          } else {
            fullpath = path
          }
        } else {
          fullpath = `${root}${path}`
        }
        
        # 绑定router
        if (middleware) {
          # 这里才是执行express逻辑的地方
          router[method](fullpath, middleware, handle)
        } else {
          router[method](fullpath, handle)
        }
      }
    }
  }
}

上面还缺一个中间件的装饰器:

js 复制代码
# 定义中间件装饰器
export function use(middleware: RequestHandler) {
  return function (target: any, key: string) {
    Reflect.defineMetadata('middleware', middleware, target, key)
  }
}

# 获取中间件
const middleware: RequestHandler = Reflect.getMetadata('middleware', target.prototype, key)

理解了上面的代码,对于理解nestjs的代码是比较容易的。

因为,nestjs 的实现原理就是通过装饰器给 class 或者对象添加元数据,然后初始化的时候取出这些元数据,进行依赖的分析,然后创建对应的实例对象就可以了。

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax