Nest 系列 - 2:装饰器和类型解析
上一节,我们留了个问题:
但我们知道 类型源于 TS,而真正运行的时候被转换成 JS 是不带有类型的,那框架层「factory」又是如何做类型匹配的呢?
在探讨这个问题之前先来看看什么是装饰器
装饰器
建议结合官网一起看 https://www.typescriptlang.org/docs/handbook/decorators.html
装饰器首先是一个函数,其次他的用法不同于一般的函数调用而是用
function xxx() {}
@xxx
class A {}
其次这个函数的使用场景有限,仅在用类的场景,并且操作对象是下面其中的内容
- Class
- Method
- Accessor
- Property
- Parameter
若几个场景同时存在,执行顺序如下
There is a well defined order to how decorators applied to various declarations inside of a class are applied:
- Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.
- Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.
- Parameter Decorators are applied for the constructor.
- Class Decorators are applied for the class.
@classDecorator
class Point {
[x: string]: any
@functionDecorator()
print() {}
@functionDecorator()
static staticPrint() {}
// 因为我们的 functionDecorator2 ,本身便是装饰器
@functionDecorator2
print2() {}
}
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
console.log("run classDecorator", constructor)
return class extends constructor {
newProperty = "new property"
}
}
// 生成装饰器的工厂函数
function functionDecorator() {
console.log("run functionDecorator")
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// target 实例方法是实例, 静态方法是类的构造函数
console.log("run functionDecorator", target, propertyKey, descriptor)
}
}
function functionDecorator2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// target 实例方法是实例, 静态方法是类的构造函数
console.log("run functionDecorator2", target, propertyKey, descriptor)
}
run functionDecorator
run functionDecorator {} print {
value: [Function: print],
writable: true,
enumerable: false,
configurable: true
}
run functionDecorator2 {} print2 {
value: [Function: print2],
writable: true,
enumerable: false,
configurable: true
}
run functionDecorator
run functionDecorator [class Point] staticPrint {
value: [Function: staticPrint],
writable: true,
enumerable: false,
configurable: true
}
run classDecorator [class Point]
⚠️注意:这些行为是在 声明 class 的时候就进行的
Metadata
https://www.typescriptlang.org/docs/handbook/decorators.html#metadata
TS在编译过程中会去掉原始数据类型相关的信息,将TS文件转换为传统的JS文件以供JS引擎执行。但是,一旦我们引入reflect-metadata并使用装饰器语法对一个类或其上的方法、属性、访问器或方法参数进行了装饰,那么TS在编译后就会自动为我们所装饰的对象增加一些类型相关的元数据
- 类型元数据使用元数据键”design:type”
- 参数类型元数据使用元数据键”design:paramtypes”
- 返回值类型元数据使用元数据键”design:returntype”
import "reflect-metadata"
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(Reflect.getMetadata("design:type", target, propertyKey))
console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey))
console.log(Reflect.getMetadata("design:returntype", target, propertyKey))
}
class Point {
constructor() {}
@validate
setX(x: number, y: string): number {
return "1" as any
}
}
const p = new Point()
[Function: Function]
[ [Function: Number], [Function: String] ]
[Function: Number]
可以看到,我们在运行时,拿到了TS环境中的类型,这样就解决了文章开头所提的如何在运行时拿到类型的问题
如果我们想自己设置一些值在 matadata 中可以
// 还未成为标准,因此想使用reflect-metadata中的方法就需要手动引入该库,引入后相关方法会自动挂在Reflect全局对象上
import 'reflect-metadata'
class Example {
text: string
}
// 定义一个exp接收Example实例,: Example/: string提供给TS编译器进行静态类型检查,不过这些类型信息会在编译后消失
const exp: Example = new Example()
// 注意:手动添加元数据仅为展示reflect-metadata的使用方式,实际上大部分情况下应该由编译器在编译时自动添加相关代码
// 为了在运行时也能获取exp的类型,我们手动调用defineMetadata方法为exp添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'Example', exp)
// 为了在运行时也能获取text属性的类型,我们手动调用defineMetadata方法为exp的属性text添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'String', exp, 'text')
// 运行时调用getMetadata方法,传入希望获取的元数据key以及目标就可以得到相关信息(这里得到了exp以及text的类型信息)
// 输出'Example' 'String'
console.log(Reflect.getMetadata('type', exp))
console.log(Reflect.getMetadata('type', exp, 'text'))
编译结果探究
利用 tsc 编译上面的文件
"use strict"
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc)
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r
return c > 3 && r && Object.defineProperty(target, key, r), r
}
exports.__esModule = true
require("reflect-metadata")
function validate(target, propertyKey, descriptor) {
console.log(Reflect.getMetadata("design:type", target, propertyKey))
console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey))
console.log(Reflect.getMetadata("design:returntype", target, propertyKey))
}
var Point = /** @class */ (function () {
function Point() {}
Point.prototype.setX = function (x, y) {
return "1"
}
__decorate([validate], Point.prototype, "setX")
return Point
})()
var p = new Point()
看到这里 我们可以理解官网中
As such, the following steps are performed when evaluating multiple decorators on a single declaration in TypeScript:
- The expressions for each decorator are evaluated top-to-bottom.
- The results are then called as functions from bottom-to-top.
的含义了 1的部分就是 when composing functions f and g, the resulting composite (f ∘ g)(x) is equivalent to f(g(x)). 2的部分就是
for (var i = decorators.length - 1; i >= 0; i--)
__decorate([validate], Point.prototype, "setX")
这一块更详细的部分可以参考 Nest.js入门 —— TS装饰器与元数据(二) 的第四节