初探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 分钟前
使用 Chrome Flags 设置(适用于 HTTP 站点开发)
前端·chrome·http
Tz一号1 小时前
前端 git规范-不同软件(GitHub、Sourcetree、WebStorm)、命令行合并方式下增加 --no-ff的方法
前端·git·github
Loadings1 小时前
MCP从理解到实现
前端·cursor·ai 编程
冬冬小圆帽1 小时前
防止手机验证码被刷:React + TypeScript 与 Node.js + Express 的全面防御策略
前端·后端·react.js·typescript
Cmoigo1 小时前
React Native自定义View(Android侧)
前端·react native
LanceJiang2 小时前
文本溢出移入Tooltip提示,我的LeText组件
前端·vue.js
moreface2 小时前
uni.request 配置流式接收+通义千问实现多轮对话
前端·vue.js·人工智能
大元992 小时前
前端必须知道的emoji知识
前端
不服就干2 小时前
js通过游览器启动本地的绿色软件
前端·javascript
零零壹112 小时前
理解Akamai EdgeGrid认证在REST API中的应用
前端·后端