最近在学习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")
参考
往期年度总结
往期文章
- 这是你所知道的ts类型断言和类型守卫吗?
- TypeScript官网内容解读
- 经常使用ts的你,知道这些内容?
- 你有了解过原生css的scope?
- 现在比较常用的移动端调试你知道哪些?
- 众多跨标签页通信方式,你知道哪些?(二)
- 众多跨标签页通信方式,你知道哪些?
- 反调试吗?如何监听devtools的打开与关闭
- 因为原生,选择一家公司(前端如何防笔试作弊)
- 结合开发,带你熟悉package.json与tsconfig.json配置
- 如何优雅的在项目中使用echarts
- 如何优雅的做项目国际化
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )
专栏文章
结语
本篇文章到此就结束了,欢迎在评论区交流。
🔥如果此文对你有帮助的话,欢迎💗关注 、👍点赞 、⭐收藏 、✍️评论, 支持一下博主~
公众号:全栈追逐者,不定期的更新内容,关注不错过哦!