ts装饰器的那点东西

最近在学习nestjs,内部都是通过装饰器去做得逻辑封装。以前也专门学习过ts的装饰器(TypeScript学习总结,并爬取一些表情包做练习(仅用作学习)),但是没有过多的实操,最近想要理解nestjs的一些原理,就打算重新研究一下ts装饰器。

装饰器简单的用法这里就不介绍了,不明白的可以看这里 TypeScript学习总结,并爬取一些表情包做练习(仅用作学习)

装饰器

装饰器就是解决在不修改原来类、方法,属性,参数的时候为其添加额外的功能。

Nestjs 中装饰器可以解决依赖注入(使用和创建分离)的问题。有了依赖注入,能大大降低项目的耦合度,提升项目的可扩展性。

装饰器注意事项

类装饰器

装饰器参数(类)

类装饰器如果返回一个类,那么创建类的时候就使用这个装饰器返回的类,如果未返回则表示依旧是当前修饰的这个类。

ts 复制代码
function decorator(constructor: any) {
  return Object // 返回其他构造函数,那么初始化Person就不会初始化Person,而是初始化返回的类
}

@decorator
class Person {
  constructor() {
    console.log("Person初始化...") // 未打印
  }
}

const p = new Person()
console.log("p", p) // {}

这里我们可以使用typesrcipt在线编译查看一下编译后的代码,可以很清楚的看出装饰器的从后往前执行,并且类装饰器返回值会覆盖当前修饰的类。(这里我们使用v4版本,编译出的代码比较清晰。)

这里我们再看一个小例子,我们将当前类增加日志信息。

ts 复制代码
// 泛型工厂类继承装饰器
interface ClassInter { new (...args: any[]): any }
function ClassFunctionExtends<T extends ClassInter> (Class: T) {
  console.log("Class", Class);
  // 继承自修饰的类
  class Logger extends Class {
    constructor(...args: any[]) {
      console.log("增加前日志信息");
      super(...args);
      console.log("增加后日志信息");
    }
  }
  // 初始化修饰的类时是执行这个Logger类,间接初始化修饰的类
  return Logger
}

// 目标类
@ClassFunctionExtends
class Test {
  // 先执行原来构造函数
  constructor(private name: string) {
    this.name = name;
  }
  say() {
    console.log(this.name, "say");
  }
}
let test=new Test("zh");

test.say()

方法装饰器

装饰器参数(原型,方法名,方法描述对象)

下面我们来将一个方法参数统一增加点内容,达到拦截器的效果。

ts 复制代码
class Person {
  constructor() {
  }

  @MethodInterceptor
  say(p1: string, p2: string) {
    console.log("say.....p1, p2", p1, p2);
  }
}

function MethodInterceptor(ClassPrototype: any, methodName: any,
  methodDesc: PropertyDescriptor){
    // 保存以前的函数体
    let origin = methodDesc.value;
    console.log("origin:", origin);

    // 修改函数体
    methodDesc.value = function (...args: any[]) {
      console.log("this:", this); // Person
      // 迭代所有参数
      args = args.map((arg) => arg + "增加相同内容")
      console.log("args", args) // [ 'zh增加相同内容', 'llm增加相同内容' ]

      // 调用以前的函数
      origin.apply(this, args)
    }
}

const p = new Person()
p.say("zh", "llm")

下面来看一下底层实现。

装饰器中只是上面拦截器中把description中的value修改了而已。所以调用方法就增加了拦截方法。

需要注意的是,访问器装饰器(即在getter,setter方法上使用装饰器)getter,setter只能有一个函数上面写装饰器,另一个上面就不能出现装饰器了。

属性装饰器

装饰器参数(原型,属性名)

这个没啥好讲的,后面我们介绍元数据的时候有用,可以获取到当前装饰的属性类型,可以用于创建对象,进行依赖注入。

(构造)方法参数装饰器

构造方法参数装饰器, 装饰器参数(类,undefined,方法参数索引)

方法参数装饰器, 装饰器参数(原型,方法名称,方法参数索引)

这个没啥好讲的,后面我们介绍元数据的时候有用,可以获取到当前装饰的方法参数类型,可以用于创建对象,进行依赖注入。

装饰器执行过程

属性装饰器 -> (方法参数装饰器 -> 方法装饰器) -> 构造器参数装饰器 -> 类装饰器。

多个方法,按照方法定义的先后顺序执行。

ts 复制代码
function ClassDe(...args: any) {
  console.log("类装饰器", args)
}

function ConsPramsDe(...args: any) {
  console.log("类构造函数参数装饰器", args)
}

function PropDe(...args: any) {
  console.log("属性装饰器", args)
}

function MethodDe(...args: any) {
  console.log("方法装饰器", args)
}
function MethodDe2(...args: any) {
  console.log("方法装饰器2", args)
}
function MethodParams(...args: any) {
  console.log("方法参数参数装饰器", args)
}
function MethodParams2(...args: any) {
  console.log("方法参数参数装饰器", args)
}
@ClassDe
class Person {

  @PropDe
  private age!: number

  constructor(@ConsPramsDe private name: string) {

  }

  @MethodDe
  say(@MethodParams name: string) {

  }
  
  @MethodDe2
  say2(@MethodParams2 name2: string) {

  }
}

这里可以看出,任意一个内容的装饰器执行都需要等待当前大类执行完毕后才能执行下一个大类(除了方法(参数和方法看成一个整体,即当前一个大类,只是参数在前方法在后)的修饰外,其他的都是按照大类先后执行)。(属性,方法(参数 -> 方法),构造器参数,类)

元数据装饰器

元数据就是附加在对象、类、方法、属性丶参数上的数据。元数据用来帮助提供实现某种业务功能需要用到的数据。 一般装饰器中使用的比较多,例如在一个方法装饰器中定义,在类装饰器获取使用。

我们使用reflect-metadata进行元数据的存取操作。

在对象,属性,方法中定义元数据

ts 复制代码
import "reflect-metadata"

const obj = {
  name: "zh",
  say() {
    console.log("say...")
  }
}

// 给对象定义元素据
Reflect.defineMetadata("metaObjKey", "定义一个对象的元数据", obj)
console.log(Reflect.getMetadata("metaObjKey", obj));//定义一个对象的元数据

// 在对象属性上定义和获取元数据
Reflect.defineMetadata('meta.obj.name', '给obj.name定义元数据', obj, "name");
console.log(Reflect.getMetadata('meta.obj.name', obj, "name"));// 给obj.name定义元数据

// 在对象方法上定义和获取元数据
Reflect.defineMetadata('meta.obj.say', '给obj.say定义元数据', obj, "say");
console.log(Reflect.getMetadata('meta.obj.say', obj, "say"));// 给obj.say定义元数据

// 使用 Reflect.hasMetadata 查看对象或对象属性上是否存在某个元数据
if (Reflect.hasMetadata('metadata', obj)) {
  console.log("对象存在metadata元数据");
} else {
  console.log("对象不存在metadata元数据"); // 对象不存在metadata元数据
}

if (Reflect.hasMetadata('metaObjKey', obj)) {
  console.log("对象存在metaObjKey元数据"); // 对象存在metaObjKey元数据
} else {
  console.log("对象不存在metaObjKey元数据");
}

console.log("获取当前对象定义的元数据键值:", Reflect.getMetadataKeys(obj)) // 获取当前对象定义的元数据键值: [ 'metaObjKey' ]
console.log("获取当前对象属性定义的元数据键值:", Reflect.getMetadataKeys(obj, "name")) // 获取当前对象属性定义的元数据键值: [ 'meta.obj.name' ]
console.log("获取当前对象方法定义的元数据键值:", Reflect.getMetadataKeys(obj, "say")) // 获取当前对象方法定义的元数据键值: [ 'meta.obj.say' ]

在类,类方法,类属性中定义元数据

  • 注意定义的方法和属性元数据都需要在原型上进行获取。
  • 子类可以获取到父类中方法和属性定义的元数据。
ts 复制代码
import "reflect-metadata"

// 在类上定义元数据
@Reflect.metadata('PersonKey', "类中定义的元数据")
class People {
  @Reflect.metadata("nameKey", "属性中定义的元数据")
  name = "zh"
  @Reflect.metadata("sayKey", "方法中定义的元数据")
  say() {

  }
}
// 获取类上的元数据
console.log(Reflect.getMetadata('PersonKey', People));// 类中定义的元数据

// 属性和方法获取元数据,都是在原型上获取的
console.log(Reflect.getMetadata('nameKey', People.prototype, "name"));// 属性中定义的元数据

// 获取方法上的元数据 第二个参数是原型
console.log(Reflect.getMetadata('sayKey', People.prototype, 'say'));//方法中定义的元数据

// 判断People.prototype 原型上 say 方法上是否存在sayKey元数据
if (Reflect.hasMetadata('sayKey', People.prototype, 'say')) {
  console.log("hasMetadata=>People原型上存在say方法的sayKey元数据"); // hasMetadata=>People原型上存在say方法的sayKey元数据
}


// 定义子类
class Son extends People {
  play() {

  }
}

//  子类获取父类原型上的方法 ------------ hasMetadata
if (Reflect.hasMetadata('sayKey', Son.prototype, 'say')) {
  console.log("hasMetadata=>Son原型上通过继承也获取到了say方法和say方法的 sayKey 元数据"); // hasMetadata=>Son原型上通过继承也获取到了say方法和say方法的 sayKey 元数据
}

//  获取自有元数据,但不能获取原型链上父类的元数据 ------------ hasOwnMetadata
if (Reflect.hasOwnMetadata('sayKey', Son.prototype, 'play')) {
  console.log("hasOwnMetadata=>Son原型上存在say方法的 sayKey 元数据");
} else {
  console.log("hasOwnMetadata=>Son原型上不存在say方法的 sayKey 元数据"); // hasOwnMetadata=>Son原型上不存在say方法的 sayKey 元数据
}


console.log("在子类原型上获取父类属性元数据", Reflect.getMetadata("nameKey", Son.prototype, "name")) //在子类原型上获取父类属性元数据 属性中定义的元数据

类属性和类方法内置的元数据key

  • design:type 表示获取当前修饰的属性或者方法类型。
  • design:paramtypes 表示获取当前修饰的(构造)方法参数的类型,返回一个数组。
  • design:returntype 表示获取当前修饰的方法返回值的类型。
ts 复制代码
import "reflect-metadata"

// 在类上定义元数据
@Reflect.metadata('PersonKey', "类中定义的元数据")
@Reflect.metadata('PersonKey2', "类中定义的元数据2")
class People {
  @Reflect.metadata("nameKey", "属性中定义的元数据")
  @Reflect.metadata("nameKey2", "属性中定义的元数据2")
  name = "zh"
  @Reflect.metadata("sayKey", "方法中定义的元数据")
  @Reflect.metadata("sayKey2", "方法中定义的元数据2")
  say() {}
}

console.log("获取类元数据定义的keys", Reflect.getMetadataKeys(People)) // 获取类元数据定义的keys [ 'PersonKey2', 'PersonKey' ]

console.log("获取属性元数据定义的keys", Reflect.getMetadataKeys(People.prototype, "name")) // 获取属性元数据定义的keys [ 'design:type', 'nameKey2', 'nameKey' ]
console.log("获取方法元数据定义的keys", Reflect.getMetadataKeys(People.prototype, "say")) 
// 获取方法元数据定义的keys [
//   'design:returntype', // 元数据修饰的方法的返回值类型
//   'design:paramtypes', // 元数据修饰的方法的参数类型(数组返回)
//   'design:type', // 元数据修饰的方法的类型
//   'sayKey2',
//   'sayKey'
// ]

实战内置装饰器key

  • 在方法参数和方法装饰器中定义元数据,在类装饰器中获取使用。
  • 通过design:paramtypes获取参数类型,可以初始化对象,达到依赖注入的效果。
ts 复制代码
import "reflect-metadata"

// 充当容器,存储初始化对象
const containerObj: any = {
  // userService:
}

// 参数装饰器
function Inject(ClassCons: Object, propName: string, paramsIndex: number) {
  // 获取方法参数类型
  const propTypeArr = Reflect.getMetadata("design:paramtypes", ClassCons)
  console.log("propTypeArr", propTypeArr)
  // 获取第一个参数类型(类),并初始化
  const propTypeObj = new propTypeArr[paramsIndex]()
  // 保存userService对象
  containerObj.userService = propTypeObj
}

// 方法装饰器
function Post(path: string) {
  console.log("当前请求路径", path)
  return (ClassCons: Object, methodName: string, desc: any) => {
    // 定义元数据
    Reflect.defineMetadata("path", path, ClassCons, methodName)
    // 查看当前方法参数类型
    console.log("查看当前方法参数类型", Reflect.getMetadata("design:paramtypes", ClassCons, methodName))
    console.log("查看当前方法返回值类型", Reflect.getMetadata("design:returntype", ClassCons, methodName))
  }
}

// 类装饰器
function Controller(ClassCons: any) {
  // 获取login方法元数据
  const path = Reflect.getMetadata("path", ClassCons.prototype, "login")
  console.log("获取到path", path)
}

class UserService {
  constructor() {
    console.log("第一个业务类初始化...")
  }

  login() {
    console.log("连接数据库进行登录查询")
  }
}

@Controller
class UserController {
  constructor(@Inject private userService?: UserService) {
    console.log("UserController初始化...")
  }

  @Post("/login")
  login(username: string, pwd: string) {
    console.log("登录接口...")
    // 拿到外部初始化的对象
    const userService = containerObj.userService
    userService.login()
  }
}


const userController = new UserController()
userController.login("zh", "zh")

参考

往期年度总结

往期文章

专栏文章

结语

本篇文章到此就结束了,欢迎在评论区交流。

🔥如果此文对你有帮助的话,欢迎💗关注 、👍点赞 、⭐收藏✍️评论, 支持一下博主~

公众号:全栈追逐者,不定期的更新内容,关注不错过哦!

相关推荐
百万蹄蹄向前冲1 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5812 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路2 小时前
GeoTools 读取影像元数据
前端
ssshooter3 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友3 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry3 小时前
Jetpack Compose 中的状态
前端
dae bal4 小时前
关于RSA和AES加密
前端·vue.js
柳杉4 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog4 小时前
低端设备加载webp ANR
前端·算法
LKAI.5 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi