reflect-metadata 依赖注入 (nest.js 的实现)

188 阅读3分钟

上篇文章,预备知识:装饰器

reflect-metadata 是一个用于在 JavaScript 中实现反射元数据的库,主要用于 TypeScript 项目。可以为 ts 项目提供元数据 metadata可以理解为“描述数据的数据”,比如你可以为类、方法、属性等附加一些额外的信息,然后在运行时读取这些信息来实现某些功能。它允许在运行时访问和操作类、方法、属性等的元数据,常用于依赖注入、序列化、ORM 等场景。

有很多的库中使用到了装饰器,需要依赖这个库,比如:typeorminversify 等。

安装

pnpm add reflect-metadata

配置

tsconfig.json 中启用装饰器和元数据支持:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

API

// 在对象或属性上定义元数据
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// 检查元数据键是否存在于对象或属性的原型链上
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// 检查对象或属性自身是否存在特定元数据键
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);

// 获取对象或属性的原型链上某个元数据键的元数据值
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// 获取对象或属性自身某个元数据键的元数据值
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);

// 获取对象或属性原型链上的所有元数据键
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);

// 获取对象或属性自身的所有元数据键
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);

// 删除对象或属性上的元数据
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// 使用装饰器为构造函数应用元数据
@Reflect.metadata(metadataKey, metadataValue)
class C {
  // 使用装饰器为方法(属性)应用元数据
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

简单使用

Reflect Metadata 的 API 可以用于类或者类的属性上,如:

import 'reflect-metadata';

class MyClass {
  @Reflect.metadata('get', 'getValue')
  @Reflect.metadata('post', 'postValue')
  myMethod() {
    console.log('myMethod');
  }
}

const metadataValue = Reflect.getMetadata('get', MyClass.prototype, 'myMethod');
const metadataValue1 = Reflect.getMetadata('post', MyClass.prototype, 'myMethod');
console.log(metadataValue); // 输出: getValue
console.log(metadataValue1); // 输出: postValue

使用场景

1. 为方法附加元数据,表示它的访问权限。

import 'reflect-metadata';

function Role(role: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    Reflect.defineMetadata('role', role, target, key);
  };
}

class UserController {
  @Role('admin')
  deleteUser() {
    console.log('User deleted');
  }
}

// 运行时检查权限
const userController = new UserController();
const role = Reflect.getMetadata('role', userController, 'deleteUser');

if (role === 'admin') {
  userController.deleteUser(); // 只有管理员可以调用
} else {
  console.log('Access denied');
}

2. 依赖注入 (nest.js 的实现)

使用装饰器与 Reflect 联动,创建简易 DI 容器


import "reflect-metadata";

/** 检查目标类是否被标记为可注入 */
function validate(target: any) {
  const isInjectable = Reflect.getMetadata("injectable", target);
  if (!isInjectable) {
    throw new Error(`${target.name} is not marked as injectable!`);
  }
}

// 标记类为可注入
function Injectable() {
  return function (target: any) {
    Reflect.defineMetadata("injectable", true, target);
  };
}

// 标记需要注入的依赖
function Inject(token: any) {
  return function (target: any, _propertyKey: any, index: number) {
    //  每个依赖都存储在 dependencies 数组中,索引对应参数的位置
    const dependencies = Reflect.getMetadata("dependencies", target) || [];
    dependencies[index] = token;
    Reflect.defineMetadata("dependencies", dependencies, target);
  };
}

// 简单的 DI 容器
class DIContainer {
  static resolve<T extends new (...args: any[]) => any>(
    target: T
  ): InstanceType<T> {
    // 检查目标类是否被标记为可注入
    validate(target);

    // 获取目标类的依赖
    const dependencies = Reflect.getMetadata("dependencies", target) || [];

    // 创建依赖实例
    const injections = dependencies.map((dep: any) => {
      // 检查目标类是否被标记为可注入
      validate(dep);
      return new dep();
    });

    // 创建目标类的实例
    return new target(...injections);
  }
}

@Injectable()
class Service {
  getMessage() {
    return "Hello from Service!";
  }
}

@Injectable()
class Controller {
  constructor(@Inject(Service) private service: Service) {}

  printMessage() {
    console.log(this.service.getMessage());
  }
}

const controller = DIContainer.resolve(Controller);
controller.printMessage(); // 输出:Hello from Service!
  • @Injectable:标记一个类可被注入。
  • @Inject:在构造函数中标记依赖。
  • DIContainer.resolve:自动根据依赖创建实例并注入。

流程解析

  1. ControllerService 类通过 @Injectable 标记为可注入。
  2. Controller 构造函数通过 @Inject(Service) 指定需要 Service 作为依赖。
  3. 调用 DIContainer.resolve(Controller)
    • 读取 Controller 的依赖元数据,发现它依赖 Service
    • 创建 Service 实例。
    • Service 实例传递给 Controller 的构造函数。
  1. 最终返回一个注入了 ServiceController 实例。

官方文档文档: rbuckton.github.io/reflect-met…