基于 Typescript 和 Decorator 实现依赖注入

简介: ## 什么是依赖注入 依赖注入是将一个对象所依赖的其他对象直接提供给这个对象,而不是在当前对象中直接构建这些依赖的对象。 ## 为什么要使用依赖注入 - 便于单元测试 - 解耦,统一管理被依赖对象的实例化,不用在类的内部创建被依赖对象 ## 如何实现依赖注入 ### Typescript 中的装饰器 Decorator 装饰器是一种特殊类型的声明,它能够被附

什么是依赖注入

依赖注入是将一个对象所依赖的其他对象直接提供给这个对象,而不是在当前对象中直接构建这些依赖的对象。

为什么要使用依赖注入

  • 便于单元测试
  • 解耦,统一管理被依赖对象的实例化,不用在类的内部创建被依赖对象

如何实现依赖注入

Typescript 中的装饰器 Decorator

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。 装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。例如,有一个@sealed装饰器,我们会这样定义和使用sealed函数:

function sealed(target) {
    // do something with "target" ...
}

@sealed
class MyClass {}

装饰器工厂

装饰器工厂就是一个简单的函数,它返回一个装饰器。
我们可以通过下面的方式来写一个装饰器工厂函数:

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

方法、属性、访问器的装饰器

方法、属性、访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。

对于属性装饰器,第3个参数为 undefined。

Typescript 中的 Reflect Metadata

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,要使用 reflect metadata,你需要:

  • npm i reflect-metadata --save
  • tsconfig.json 里配置 emitDecoratorMetadata 选项为 true
{
  "compilerOptions": {
    "target": "ES2015",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

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

function metadata(
  metadataKey: any,
  metadataValue: any
): {
  (target: Function): void;
  (target: Object, propertyKey: string | symbol): void;
};

Reflect.metadata 可以当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

@Reflect.metadata('inClass', 'A')
class Test {
  @Reflect.metadata('inMethod', 'B')
  public hello(): string {
    return 'hello world';
  }
}

console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'

Reflect metadata 有强大的功能,包括获取类类型信息和自定义元数据信息以及获取自定义元数据信息。

获取类型信息

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`); // Aprop type: string
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}

在装饰器函数中可以通过下列三种内置的 metadataKey 获取类型信息。

  • design:type: 属性类型
  • design:paramtypes: 参数类型
  • design:returntype: 返回值类型

自定义 metadataKey

Reflect Metadata 除能获取内置类型信息外,还可用于自定义 metadataKey,并在合适的时机获取它的值,示例如下:

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

基于 typescript 实现依赖注入以及 Controller Get 装饰器

在前面我们介绍了 typescript 中的 decorator 和 reflect-metadata。这些都是为实现依赖注入做的基础准备,下面将介绍如何基于以上技术实现依赖注入。以及基于 decorator 实现 node web 框架中的 Controller Get 等装饰器。

通过构造函数注入

import 'reflect-metadata';

type Constructor<T=any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => { };

class OtherService {
  a = 1;
}

// 通过构造函数注入
@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) { }

  testMethod() {
    console.log(this.otherService);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target);
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // OtherService {a: 1}

上面是一个简单的通过构造函数实现依赖注入的例子。这里可能会有一个疑问,装饰器 Injectable() 似乎什么都没做。但是,把 Injectable() 装饰器去掉后,我们就无法实现依赖注入了。原因是什么呢?我们可以去编译后的代码看下:

// 通过构造函数注入
var TestService = /** @class */ (function () {
    function TestService(otherService) {
        this.otherService = otherService;
    }
    TestService.prototype.testMethod = function () {
        console.log(this.otherService);
    };
    TestService = __decorate([
        Injectable(),
        __metadata("design:paramtypes", [OtherService])
    ], TestService);
    return TestService;
}());

只有添加了 Injectable() 装饰器后才有下面这段代码,这段代码将构造函数的参数类型信息存储到了 metadata 中,使得之后在实例化时能够获取到构造函数参数的类型。

TestService = __decorate([
    Injectable(),
    __metadata("design:paramtypes", [OtherService])
], TestService);

通过类成员方法参数注入

// 通过类成员方法参数注入
const MethodInjectable = (): MethodDecorator => (
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<any>
) => {
  let method = descriptor.value;
  descriptor.value = function (...args) {
    const providers = Reflect.getMetadata('design:paramtypes', target, propertyKey);
    const providersInsts = providers.map(P => new P());

    return method.apply(this, [...providersInsts, ...args]);
  }
};

class TestParamInjectService {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  @MethodInjectable()
  greet(otherService: OtherService) {
    console.log(otherService);
  }
}

const test = new TestParamInjectService('test');
test.greet();  // OtherService {a: 1}

在类成员方法参数依赖注入中,我们用到了类成员方法装饰器中的描述符 descriptor。descriptor.value 为该成员方法的值,我们要修改类成员方法,修改 descriptor.value 即可。

Controller 与 Get 的实现(基于 Decorator)

如果你在使用 TypeScript 开发 Node 应用,例如基于 nestjs 开发 node web 应用,相信你对 Controller、Get、POST 这些 Decorator,并不陌生:

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {

  }
}
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';

function Controller(path: string): ClassDecorator {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {

  }
}

function isConstructor(f) {
  try {
    new f();
  } catch (err) {
    if (err.message.indexOf('is not a constructor') >= 0) {
      return false;
    }
  }
  return true;
}

function isFunction(functionToCheck) {
  return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);

  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
    .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    };
  });
}

// 得到一些有用的信息
Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'

const routes = mapRoute(new SomeClass());
console.log(JSON.stringify(routes));

输出结果如下:

[
    {
        "route": "/a",
        "method": "GET",
        "methodName": "someGetMethod"
    },
    {
        "route": "/b",
        "method": "POST",
        "methodName": "somePostMethod"
    }
]

通过 Reflect.getMetadata 将类的路由取出。通过 mapRoute 将存储在成员函数上的路由和方法信息提取出来,映射成 route,提取出有用的信息。最后,只需把 route 相关信息绑在 express 或者 koa 上就 ok 了。

更多

  • 自动扫描
  • 循环依赖问题的解决

在 java spring 中,完整的依赖注入还需要自动扫描功能,在 spring 应用启动的时候,会自动扫描@Injectable 并自动完成注入工作,而不需要Factory(TestService)这么写了。
在自动扫描的过程中,我们需要注意到不能出现循环依赖,或者在扫描过程中处理掉循环依赖问题。要解决循环依赖问题,可以将服务的依赖关系构造成一个有向图,具体实现是先将当前的服务推入栈中,再逐层递归遍历服务的依赖插入图中(深度优先遍历)。有向图中存在环则存在循环依赖。有向图构造出来之后拿出图中所有出度构成的依赖数组,因为依赖关系是逐层往上的,即将 A 服务所依赖的其他服务依次实例化,最后再实例化 A ,一直到全部实例化完成为止。

nestjs 源码研究

在 nestjs 中,大量使用了 decorator 和 reflect metadata,如果要深入研究,那么可以深入到其源码中进行分析研究。

参考文献

相关文章
|
5月前
|
JavaScript 监控 编译器
29.【TypeScript 教程】装饰器(Decorator)
29.【TypeScript 教程】装饰器(Decorator)
46 0
|
存储 编译器 索引
学习TypeScript20(装饰器Decorator)
定义一个类装饰器函数 他会把ClassA的构造函数传入你的watcher函数当做第一个参数
104 0
学习TypeScript20(装饰器Decorator)
|
2月前
|
JavaScript
typeScript进阶(9)_type类型别名
本文介绍了TypeScript中类型别名的概念和用法。类型别名使用`type`关键字定义,可以为现有类型起一个新的名字,使代码更加清晰易懂。文章通过具体示例展示了如何定义类型别名以及如何在函数中使用类型别名。
36 1
typeScript进阶(9)_type类型别名
|
18天前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
28 0
|
18天前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与实用技巧
【10月更文挑战第8天】深入理解TypeScript:类型系统与实用技巧
|
2月前
|
JavaScript
typeScript基础(5)_对象的类型-interfaces接口
本文介绍了TypeScript中接口(interfaces)的基本概念和用法,包括如何定义接口、接口的简单使用、自定义属性、以及如何使用`readonly`关键字定义只读属性。接口在TypeScript中是定义对象形状的重要方式,可以规定对象的必有属性、可选属性、自定义属性和只读属性。
38 1
|
2月前
|
存储 JavaScript
typeScript进阶(11)_元组类型
本文介绍了TypeScript中的元组(Tuple)类型,它是一种特殊的数组类型,可以存储不同类型的元素。文章通过示例展示了如何声明元组类型以及如何给元组赋值。元组类型在定义时需要指定数组中每一项的类型,且在赋值时必须满足这些类型约束。此外,还探讨了如何给元组类型添加额外的元素,这些元素必须符合元组类型中定义的类型联合。
42 0
|
2月前
|
JavaScript
typeScript进阶(10)_字符串字面量类型
本文介绍了TypeScript中的字符串字面量类型,这种类型用来限制变量只能是某些特定的字符串字面量。通过使用`type`关键字声明,可以确保变量的值限定在预定义的字符串字面量集合中。文章通过示例代码展示了如何声明和使用字符串字面量类型,并说明了它在函数默认参数中的应用。
33 0
|
1天前
|
JavaScript 开发者
在 Babel 插件中使用 TypeScript 类型
【10月更文挑战第23天】可以在 Babel 插件中更有效地使用 TypeScript 类型,提高插件的开发效率和质量,减少潜在的类型错误。同时,也有助于提升代码的可理解性和可维护性,使插件的功能更易于扩展和升级。
|
13天前
|
JavaScript 前端开发
TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第11天】TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!