元数据(Metadata)
Nest 依赖注入核心 api: Reflect 的 metadata (草案阶段):
typescript
// 定义元数据,没有指定属性键,因此应用于整个目标对象
Reflect.defineMetadata(metadataKey, metadataValue, target);
// 定义元数据,指定了属性键,因此只应用于目标对象的特定属性
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
// 检索元数据,没有指定属性键,因此从整个目标对象中检索
let result = Reflect.getMetadata(metadataKey, target);
// 检索元数据,指定了属性键,因此从目标对象的特定属性中检索
let result = Reflect.getMetadata(metadataKey, target, propertyKey);
Reflect.defineMetadata 和 Reflect.getMetadata 分别用于设置和获取某个类的元数据,如果最后传入了 propertyKey 属性名(可选),还可以单独为某个属性设置元数据。
如果给类或者类的静态属性添加元数据,那就保存在类上。
如果给实例属性添加元数据,那就保存在对象上,用类似 [[metadata]] 的 key 来存的。
要使用这个特性,首先安装 reflect-metadata 库:
bash
npm install reflect-metadata
tsconfig.json
文件这样设置:
json
{
"compilerOptions": {
"target": "es2016", // 指定 ECMAScript 目标版本:这里是 ES2016,也就是 ES7。这意味着 TypeScript 会将代码编译成符合 ES2016 标准的 JavaScript。
"module": "commonjs", // 指定生成哪种模块系统代码。这里是 CommonJS,适用于 Node.js 环境或者早期的 JavaScript 模块加载方案。
"experimentalDecorators": true, // 启用实验性的装饰器特性。装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问符、属性或参数上。装饰器使用 `@expression` 形式,`expression` 求值后必须为一个函数,它会在运行时被调用。
"emitDecoratorMetadata": true, // 启用后,编译器会为装饰器提供源代码中类型的元数据支持。这通常用于反射机制的实现,比如在 Angular 的依赖注入中。
"esModuleInterop": true // 启用模块的 ES6 互操作性,允许默认导入从没有使用默认导出的模块。
}
}
再安装 typescript:
bash
npm install typescript -g
安装插件:
运行代码:
代码中这样使用:
Reflect.metadata 装饰器当然也可以再封装一层:
这样使用:
@Module 装饰器的实现,就调用了 Reflect.defineMetadata 来给这个类添加了一些元数据:
typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
后面创建 IOC 容器的时候就会取出这些元数据来处理。
Nest 的实现原理就是通过装饰器给 class 或者对象添加元数据,然后初始化的时候取出这些元数据,进行依赖分析,创建对应的实例对象就可以了。
所以说,nest 实现的核心就是 Reflect metadata,但是目前这个 api 还处于草案阶段,需要使用 reflect-metadata 这个 polyfill 包才行。
还有疑问,依赖的扫描可以通过 metadata 数据,但是创建的对象需要知道构造器的参数,现在并没有添加这部分 metadata 数据。
比如这个 CatsController 依赖了 CatsService,但是并没有添加 metadata:
typescript
import { Injectable, Controller } from '@nestjs/common';
import { CatsService } from './cats.service';
@Injectable()
class CatsService {
// CatsService 的实现...
}
@Controller('cats')
class CatsController {
constructor(private catsService: CatsService) {}
// CatsController 的实现...
}
这其实是 TS 支持编译时自动添加一些 metadata 数据(tsconfig.json
开启 **emitDecoratorMetadata **选项)。
比如下面代码:
typescript
@Controller('cats')
class CatsController {
constructor(private catsService: CatsService) {}
// CatsController 的实现...
}
编译后的 JavaScript 代码可能会看起来像这样:
typescript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
// 简化的装饰器应用逻辑
};
var __metadata = (this && this.__metadata) || function (k, v) {
// 简化的元数据应用逻辑
};
require("reflect-metadata");
var decorators_1 = require("./decorators");
var cats_service_1 = require("./cats.service");
let CatsController = class CatsController {
constructor(catsService) {
this.catsService = catsService;
}
};
CatsController = __decorate([
decorators_1.Controller('cats'),
__metadata("design:paramtypes", [cats_service_1.CatsService])
], CatsController);
关键点在于 __metadata("design:paramtypes", [cats_service_1.CatsService])
这一行,它使用了 __metadata
函数来定义了 CatsController
构造函数的参数类型。
创建对象的时候可以通过 design:paramtypes
拿到构造器参数的类型了,这样就能正确的注入依赖了。
如果装饰的是一个方法。
编译后会看到多了三个元数据:
- design:type 是 Function:描述装饰目标的元数据是函数
- design:paramtypes 是 [Number]:参数的类型
- design:returntype 是 String:返回值的类型
这就是 nest 的核心实现原理:
- 通过装饰器给 class 或者对象添加 metadata,并且开启 ts 的 emitDecoratorMetadata 来自动添加类型相关的 metadata,然后运行的时候通过这些元数据来实现依赖的扫描,对象的创建等等功能。
Nest 的装饰器都是依赖 reflect-metadata 实现的。
而且还提供了一个 @SetMetadata 的装饰器让我们可以给 class、method 添加一些 metadata。这个装饰器的底层实现自然是 Reflect.defineMetadata。
@SetMetadata 搭配 reflector
新建个项目:
typescript
nest new metadata-and-reflector -p npm
创建 guard 和 interceptor:
bash
nest g interceptor aaa --flat --no-spec
nest g guard aaa --flat --no-spec
使用 guard 和 interceptor,并用 @SetMetadata 加个 metadata:
通过 reflector.get 取出 handler 上的 metadata,guard 使用:
interceptor 里也是这样,这里换种属性注入方式:
刷新下页面,就可以看到已经拿到了 metadata:
我们拿到 metadata 后可以判断权限,比如这个路由需要 admin 角色,取出 request 的 user 对象,看看它有没有这个角色,有才放行。
除了能拿到 handler 上的装饰器,也可以拿到 class 上的:
获取:
typescript
console.log('Interceptor', this.reflector.get('roles', context.getClass()));
reflector 还有 3 个方法:
get 的实现就是 Reflect.getMetadata。
getAll 是返回一个 metadata 的数组。
getAllAndMerge,会把它们合并为一个对象或者数组。
getAllAndOverride 会返回第一个非空的 metadata。