前言
在学习nestjs的过程中,Reflect-Metadata很重要,因为nestjs主要是用装饰器来实现各种功能,装饰器可以通过声明的方法,在定义类的时候,扩充类成员和类成员的能力。
Reflect-Metadata与装饰器配合使用能够很方便的实现在框架开发中的如路由/控制器的注册和管理,通过元数据来判断对某个方法或属性的访问权限,对属性添加校验的功能。那么为什么要用元数据去做这些工作呢?
- 首先元数据可以在不用修改原始代码(这点很重要,不会污染源代码)的情况下,动态的决定某些动作,如通过元数据对方法进行权限控制。
- 其次Reflect-matadata可以很方便的在运行时操作,如标记类的构造函数参数或属性,运行时会自动解析并注入依赖。
- API统一,由reflect-metadata统一提供。
- 与装饰器配合使用更加强大, 1 + 1 > 2。
- Reflect-Metadata是 NestJS 实现依赖注入、装饰器的核心,存取数据很多都是通过它。
Reflect-Metadata作为ECMA的提案(还没有实现纳入标准),但是Typescript早已支持了Reflect-Metadata相关的功能。再看例子之前,先要明白一个概念,什么是元数据? 元数据其实就是描述数据的数据。
提案介绍: rbuckton.github.io/reflect-met…
github: github.com/rbuckton/re…
定义元数据
定义元数据的方式有两种
- 装饰器
- 使用API定义的方法
api:
function defineMetadata(metadataKey: any, metadataValue: any, target: Object): void;
function defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol): void;
代码:
import 'reflect-metadata'
class AppController {
// 1. 使用装饰器的方式
@Reflect.metadata('money', {moneny: 10000})
private userName: string
constructor(userName: string) {
this.userName = userName
}
}
const appController = new AppController('zhangsan');
// 2. 使用api
// 给userName定义元数据
Reflect.defineMetadata('xingge','kailang',appController,'userName');
export default AppController
查看元数据
查看元数据是否存在
api:
function hasMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;
代码:
// 查看userName是否存在元数据
const hasUserNameMetadata1 = Reflect.hasMetadata('xingge',appController,'userName')
const hasUserNameMetadata2 = Reflect.hasMetadata('money',appController,'userName')
console.log(hasUserNameMetadata1); // true
console.log(hasUserNameMetadata2); // true
获取元数据
api:
function getMetadata(metadataKey: any, target: Object): any;
function getMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): any;
代码:
// 获取userName元数据
const userNameMetaValue1 = Reflect.getMetadata('xingge',appController,'userName')
const userNameMetaValue2 = Reflect.getMetadata('money',appController,'userName')
console.log("userNameMetaValue1: ", userNameMetaValue1); // kailang
console.log("userNameMetaValue2: ", userNameMetaValue2); // { moneny: 10000 }
删除元数据
api:
function deleteMetadata(metadataKey: any, target: Object): boolean;
function deleteMetadata(metadataKey: any, target: Object, propertyKey: string | symbol): boolean;
代码:
const isDeleteSuccess1 = Reflect.deleteMetadata('xingge',appController,'userName')
const isDeleteSuccess2 = Reflect.deleteMetadata('aaaaa',appController,'userName')
console.log(isDeleteSuccess1); // true
console.log(isDeleteSuccess2); // false
获取对象上所有的元数据键
api:
function getMetadataKeys(target: Object): any[];
function getMetadataKeys(target: Object, propertyKey: string | symbol): any[];
代码:
const keys = Reflect.getMetadataKeys(appController, 'userName')
console.log(keys); // [ 'xingge', 'money' ]
还有两个api hasOwnMetadata
和 getOwnMetadata
。
这两个方法在nest中不常用,nestjs源码中也没有用到。
Reflect.getOwnMetadata()
方法只在对象本身上查找元数据。它不会沿着原型链向上搜索。
Reflect.getMetadata()
方法会首先在目标对象上查找元数据,如果没有找到,则会继续沿着原型链向上搜索,直到找到为止。如果在原型链上找到了元数据,就会返回该元数据。如果没找到则返回undefined。
定义一个UserController
类继承AppController
class UserController extends AppController {
constructor(userName: string) {
super(userName)
}
}
给getHello
定义元数据
class AppController {
@Reflect.metadata('money', {moneny: 10000})
private userName: string
constructor(userName: string) {
this.userName = userName
}
// 给getHello定义元数据
@Reflect.metadata('helloKey','helloValue')
getHello(): string {
return 'Hello World!'
}
}
分别从appController
和userController
获取getHello
的元数据
console.log(Reflect.getMetadata('helloKey', appController, 'getHello')); // helloValue
console.log(Reflect.getOwnMetadata('helloKey', appController, 'getHello')); // undefined
const userController = new UserController('lisi');
console.log(Reflect.getMetadata('helloKey', userController, 'getHello')); // helloValue
因为helloKey
是定义在父级的,getOwnMetadata
获取不到,而appController
和userController
的 getMetadata
方法都可以获取到,因为它可以沿着原型链查找。
如果非要通过getOwnMetadata
获取数据呢,那就从原型上获取
console.log(Reflect.getMetadata('helloKey', appController, 'getHello')); // helloValue
// 获取到 AppController 的 prototype
console.log(Reflect.getOwnMetadata('helloKey', Reflect.getPrototypeOf(appController)!, 'getHello')); // helloValue
const userController = new UserController('lisi');
console.log(Reflect.getMetadata('helloKey', userController, 'getHello')); // helloValue
// @ts-ignore
// 获取到UserController的原型的原型
console.log(Reflect.getOwnMetadata('helloKey', UserController.__proto__.prototype, 'getHello')); // helloValue
hasOwnMetadata
方法同理。
其实Reflec-metadata还有一个API,metadata
function metadata(metadataKey: any, metadataValue: any)
其实这个api我们已经用过了,是在类中使用的。
emitDecoratorMetadata
这个tsconfig配置项与Reflect-matadata息息相关,用来决定是否启用 编译结果是否输出元数据类型用来做反射。
ts代码
import "reflect-metadata";
function LogMethod(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
class Demo {
@LogMethod
public foo(bar: number) {
// do nothing
}
}
const demo = new Demo();
开启emitDecoratorMetadata编译后的结果
"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;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
function LogMethod(target, propertyKey, descriptor) {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
class Demo {
foo(bar) {
// do nothing
}
}
__decorate([
LogMethod,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
], Demo.prototype, "foo", null);
const demo = new Demo();
多出来的这几行代码(ts把这三个键和对应的类型信息添加到数据上)
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
- design:type 表示被装饰的对象是什么类型
- design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key
- design:returntype 表示被装饰对象的返回值属性
实现依赖注入
我们了解了design:xxx
的意思后,结合reflect-metadata
和装饰器,就可以实现依赖注入了
import "reflect-metadata";
function Injectable() {
return function (target: any) {
Reflect.defineMetadata("injectable", true, target);
};
}
class MyDependency {
doSomething() {
console.log("MyDependency doSomething执行");
}
}
class MyDependency1 {
dep2Method() {
console.log("MyDependency1 dep2Method 执行");
}
}
@Injectable()
class MyService {
constructor(private _dependency: MyDependency, private _dependency1: MyDependency1) {}
doSomething() {
this._dependency.doSomething();
this._dependency1.dep2Method();
}
}
class DependencyInjection {
static get<T>(target: any): T {
const isInjectable = Reflect.getMetadata("injectable", target);
if (!isInjectable) {
throw new Error("Target is not injectable");
}
const dependencies = Reflect.getMetadata("design:paramtypes", target) || []; // [ [class MyDependency], [class MyDependency1] ]
const instances = dependencies.map(provider => new provider());
return new target(...instances);
}
}
const myService = DependencyInjection.get<MyService>(MyService);
/**
MyDependency doSomething执行
MyDependency1 dep2Method 执行
*/
myService.doSomething();
上面使用@Injectable
装饰器进行依赖注入,不手动实例化对象,也可以使用this._dependency
获取到实例对象。
总结
通过上文我们了解了Reflect-Metadata的作用和其API。还有一些设计模式相关的东西。
Reflect-Metadata给js通过了强大的元编程的能力,而且在Nestjs源码中路由、控制器这些也是通过 Reflect-Metadata 进行灵活的配置和管理的。