初探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 或者对象添加元数据,然后初始化的时候取出这些元数据,进行依赖的分析,然后创建对应的实例对象就可以了。

相关推荐
加班是不可能的,除非双倍日工资3 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼4 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT5 小时前
promise & async await总结
前端
Jerry说前后端5 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天5 小时前
A12预装app
linux·服务器·前端