【Nest源码系列】装饰器元数据的奇妙联合(上)

一间房子里面放了一张床,这个房子满足了用来睡觉的基本需求,在这个基础上新增了沙发、红酒杯、电视机,那么就可以实现坐在沙发上摇着红酒看综艺节目的理想状态。此时沙发、红酒杯、电视机起到了装饰器的效果,在不影响房间可以用来睡觉的前提下扩展了更多的功能,实现松耦合和易扩展,这就是装饰器。

令人迷惑的装饰器参数

装饰器是贯穿Nest开发的重要部分,无论你编写模块、控制器、服务以及数据访问,都有对应的装饰器为你提供支持,比如这些:

通过调试,可以看到@UseGuards()源码是这样定义的,它接受三个参数:target、key、descriptor

还有@Controller()类装饰器和@Body()参数装饰器是这样的:

这样看好像没什么问题,我们知道目前浏览器不支持装饰器功能,还处于草案阶段,需要借助typescript的编译器或者babel进行编译才能执行,但你去翻一下ES6文档或ECMAScript提案,会发现stage-3的装饰器参数跟Nest源码中的并不相同,它们是这样的:

类装饰器:

typescript 复制代码
type ClassDecorator = (value: Function, context: {
  kind: "class";
  name: string | undefined;
  addInitializer(initializer: () => void): void;
}) => Function | void;

方法装饰器:

typescript 复制代码
type ClassMethodDecorator = (value: Function, context: {
  kind: "method";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;

属性装饰器:

typescript 复制代码
type ClassFieldDecorator = (value: undefined, context: {
  kind: "field";
  name: string | symbol;
  access: { get(): unknown, set(value: unknown): void };
  static: boolean;
  private: boolean;
}) => (initialValue: unknown) => unknown | void;

它们只接受两个参数,value和context,其中value在不同的装饰器类型下表现不同,如在类中表示被修饰的类, 在方法中表示被修饰的方法 ;context是含有丰富信息的上下文对象

于是,有朋友可能产生了以下问题:

  • 为什么会这两种版本的装饰器?出现这个差异的原因是什么?
  • 虽然装饰器还在stage阶段,但事实上有很多的框架/库基于Babel或Tsc提供了装饰器的能力,它们各自是如何看齐ES规范的?目前各自的支持程度又到了哪个阶段?

要回答这些问题,要从装饰器发展史说起!

装饰器一路走来经历了什么

我们要知道,一个JavaScript特性正式进入规范是需要经历下面五个阶段,从 Stage 0(Strawman,初稿)开始,经 Stage 1(提案)、Stage 2(草案)、Stage 3(候选提案),最后到 Stage 4(Finished,过审提案)结束。

装饰器基于这个流程之上的时间顺序是这样的:

  • 2014-04-10:Yehuda Katz向TC39提出装饰器,提案进入了stage 0阶段
  • 2015-03-24:装饰器进入到stage 1阶段,当时是放在GitHub这个仓库的,后面统一迁移到了现在这个仓库
  • 2015-07-20:TypeScript 1.5 就开始支持了第一阶段的装饰器,通过--experimentalDecorators这个配置来开启。当时Angular和MobX也用上了这个TypeScript特性。
  • 2016-07-28:提案进入了stage 2阶段
  • 2022-03-28:提案进入了stage 3阶段,同时metadata也被单独出来作为一个题案,目前也进入了stage 3阶段
  • 2023-03-16:TypeScript5.0支持了第三阶段的装饰器!

由于babel在前端领域中占有重要的份额,我觉得有必要在这里扩展补充:

  • 2015-03-31: Babel 5.0.0 支持了stage 1的装饰器
  • 2015-11-29:Babel 6 中是用 Logan Smyth 写的扩展插件支持了 stage 1 的装饰器
  • 2018-08-27:Babel 7.0.0通过官方@babel/plugin-proposal-decorators支持第二阶段装饰器
  • 截止目前为止,官方插件目前支持以下版本
    • "legacy":第一阶段装饰器
    • "2018-09":第二阶段装饰器
    • "2021-12":原第二阶段装饰器的升级版
    • "2022-03":第三阶段装饰器
    • "2023-05":第三阶段装饰器升级版
    • "2023-01":第三阶段装饰器升级版

我们可以通过babel配置或者babel repl来切换这个版本

现在是不是对装饰器有更加清晰的认识了?我们再来看看Nest中tsconfig.json中的配置,默认开启了experimentalDecorators 配置,表示使用stage 1提案的装饰器

如果设置为false,那默认使用最新stage 3的装饰器,是有两个参数。另外,需要把emitDecoratorMetadata元数据配置也设为false,因为它是依赖experimentalDecorators的,这个会在后面讲IOC思想的时候再详细介绍。

以samples下的01-cats-app为例,把tsconfig.json这两个属性修改为这样:

重新npm run start:dev启动服务

编译报错提示在运行时接收到了2个参数,实际上@Controller() 预期接收1个,而@Post() 预期接收3个,明显是使用了stage 3最新提案的参数。

装饰器类型

从前面Nest源码我们可以发现,不同类型的装饰器接收的参数不一样,作用也不同,常用的装饰器有五中类型,分别是:

  • 类装饰器
  • 方法装饰器
  • 访问器装饰器
  • 属性装饰器
  • 参数装饰器

我们来过一下:

类装饰器

装饰器可以用来装饰整个类

typescript 复制代码
/**
 * 类装饰器
 */
const doc: ClassDecorator = (target: Function) => {
    console.log("---------------类装饰器-----------------");
    target.prototype.name = 'jmin'
    console.log(target);
    console.log("---------------类装饰器-----------------");
}

@doc
class App {
    constructor() {   
    }
}

const app: Record<string, any> = new App()
console.log('app name: ' + app.name);

上面代码中,@doc用来装饰App类,同时往原型链上添加一个属性为name,taget为修饰的类。

我们通过CodePen 来编译ES6代码,它是一个社区驱动的在线代码编辑器,支持编辑 HTML、CSS 和 JavaScript。它也支持 Babel,能够将 ES6 代码转换成 ES5 并进行实时预览。

由于本节案例中我们使用typescript编码,注意需要将语言类型切换为typescript。来看看效果:

方法的装饰

装饰器可以用来装饰类的方法

typescript 复制代码
/**
 * 方法装饰器
 */
const log: MethodDecorator = (target: Object, propertyKey: string | Symbol, descriptor: PropertyDescriptor) => {
  console.log("---------------方法装饰器-----------------");
  console.log(target);
  console.log(propertyKey);
  console.log(descriptor);
  console.log("---------------方法装饰器-----------------");
}

class User {
  @log
  getName() {
    return 'jmin';
  }
}

上面代码中,@log装饰器函数修饰User类方法getName,其中target为'装饰'类的原型,上例是User.prototype,由于这时候类还没有被实例化,只能去装饰原型对象,这是不同于类装饰器的,来看看运行效果:

由于User类原型还未挂载属性方法,所以是{}

访问器装饰器

访问器装饰器可以用来修饰类的setter和getter函数,跟类方法装饰器类似,唯一区别在于它们的描述器中有的key不同

方法装饰器的描述器的key为:

  • value
  • writable
  • enumerable
  • configurable

访问器装饰器的描述器的key为:

  • get
  • set
  • enumerable
  • configurable
typescript 复制代码
/**
 * 访问器装饰器
 */
const immutable: ParameterDecorator = (target: any, propertyKey: string | Symbol, descriptor: PropertyDescriptor) => {
    console.log('-------------访问器装饰器------------------');
    console.log(target);
    console.log(propertyKey);
    console.log(descriptor);
    console.log('-------------访问器装饰器------------------');   
}
class User {
    private _name = 'jmin'
    @immutable
    get name() {
      	return this._name;
    }
}

执行结果

属性的装饰

装饰器也可以用来修饰类属性

typescript 复制代码
/**
 * 属性装饰器
 */
const prop: PropertyDecorator = (target: Object, propertyKey: string | Symbol) => {
    console.log('------属性装饰器-------');
    
    console.log(target);
    console.log(propertyKey);
    
    console.log('------属性装饰器-------');

}

class User {
    @prop
    name: string = 'jmin'
}

上面代码中,@prop用于修饰Use类的name属性,此时target为装饰类的原型对象,propertyKey为修饰的属性

参数的装饰

还可以装饰类方法参数

typescript 复制代码
/**
 * 参数装饰器
 */

const param: ParameterDecorator = (target: Object, propertyKey: string | Symbol | undefined, index: number) => {
    console.log('-------------参数装饰器------------------');
    console.log(target);
    console.log(propertyKey);
    console.log(index);
    console.log('-------------参数装饰器------------------');   
}
class User {   
    getName(@param name: string) {
      	return name;
    }
}

上面代码中,@param用于装饰类方法getName的参数,其中target为装饰类的原型,propertyKey为装饰的类方法,index为装饰方法的参数索引位置,来看看效果:

但这些装饰器只有自带的几个参数信息,比如类装饰器中只有一个target参数并不够用怎么办呢?那就要用到工厂函数来增强装饰器能力了。比如这样:

typescript 复制代码
/**
 * 类装饰器
 */
const doc: ClassDecorator = (module) => {
    return (target: Function) => {
    	console.log("---------------类装饰器-----------------");
      target.prototype.module = module
    	console.log(target);
    	console.log("---------------类装饰器-----------------");
    }
}

@doc('user')
class App {
    constructor() {   
    }
}

const app: Record<string, any> = new App()
console.log('app module: ' + app.module);

上面代码通过装饰器工厂函数增强之后,@doc可以接受外部参数,使得App被实例化的时候有能力获取更多的信息来组织代码,到这里,你是不是已经明白了Nest中的装饰器是如何实现的?它在依赖注入中扮演着重要角色。

执行时机与顺序

我们重新把前面装饰类方法时执行结果拿过来

你也许会发现,即使类没有被实例化,但装饰器依旧会执行,这是因为装饰器是在解析阶段执行的。

对于属性/方法/访问器装饰器而言,它们的执行顺序取决于声明的先后顺序。

然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行

less 复制代码
function func(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class C {
  method(
    @func("Parameter Foo") foo,
    @func("Parameter Bar") bar
  ) {}
}

执行结果为:

sql 复制代码
evaluate:  Parameter Foo
evaluate:  Parameter Bar
call:  Parameter Bar
call:  Parameter Foo

参数装饰器优先于方法装饰器执行。


less 复制代码
function func(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class C {
  @func("Method method")
  method(
    @func("Parameter Bar") bar
  ) {}
}

执行结果为:

sql 复制代码
evaluate: Method method
evaluate: Parameter Bar
call: Parameter Bar
call: Method method

了解完这几个概念后再来看看Nest中的装饰器就很好理解了,把前面令人迷惑的代码再贴过来。

现在你是不是知道了不同装饰器是有不同的工厂来处理的,接着获取到不同的元数据 通过Reflect.defineMetadata这个API存起来,等到需要的时候再拿出来使用。

小结

通过看Nest中装饰器的源码实现,发现参数个数跟最新提案的不一样,以这个契机我们顺藤摸瓜了解了规范的五个阶段,Babel通过官方@babel/plugin-proposal-decorators插件提供支持,TypeScript则通过experimentalDecorators实现了对规范的兼容和支持,我们可以通过里面的配置实现切换不同版本。

接着介绍5个常用的装饰器类型,在Nest中的装饰器无非也就这几种,它们都是通过装饰器工厂增强能力。

装饰器执行时机的先后顺序取决于声明的顺序,而参数装饰器是从最后一个开始往前执行,它们都是在解析阶段执行的。

下一节我们结合元数据来实现一个参数验证器。

相关推荐
黄毛火烧雪下1 分钟前
React Native (RN)项目在web、Android和IOS上运行
android·前端·react native
fruge6 分钟前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
baozj11 分钟前
🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
前端·javascript·vue.js
用户40993225021220 分钟前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端121 分钟前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试22 分钟前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机33 分钟前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
molly cheung1 小时前
FetchAPI 请求流式数据 基本用法
javascript·fetch·请求取消·流式·流式数据·流式请求取消
疯狂踩坑人1 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia1 小时前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc