基于 TypeScript 的 IoC 与 DI

Posted 前端外刊评论

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 TypeScript 的 IoC 与 DI相关的知识,希望对你有一定的参考价值。

前言

在使用 Angular或者 Nestjs时,你可能会遇到下面这种形式的代码:

 
   
   
 
  1. import { Component } from '@angular/core';

  2. import { OtherService } from './other.service.ts';


  3. @Component({

  4. // 组件属性

  5. })

  6. export class AppComponent {

  7. constructor(public otherService: OtherService) {

  8. // 为什么这里的otherService会被自动传入

  9. }

  10. }

上述代码中使用了 Component的装饰器,并在模块的 providers中注入了需要使用的服务。这个时候,在 AppComponentotherService将会自动获取到 OtherService实例。

你可能会比较好奇, Angular是如何实现这种神奇操作的呢?实现的过程简而言之,就是 Angular在底层使用了IoC设计模式,并利用 TypeScript强大的装饰器特性,完成了依赖注入。下面我会详细介绍IoC与DI,以及简单的DI实例。

理解了IoC与DI的原理,有助于我们更好的理解和使用 AngularNestjs

什么是 IoC?

IoC 英文全称为 Inversion of Control,即控制反转。控制反转是面向对象编程中的一种原则,用于降低代码之间的耦合度。传统应用程序都是在类的内部主动创建依赖对象,这样将导致类与类之间耦合度非常高,并且不容易测试。有了 IoC 容器之后,可以将创建和查找依赖对象的控制权交给了容器,这样对象与对象之间就是松散耦合了,方便测试与功能复用,整个程序的架构体系也会变得非常灵活。

正常方式的引用模块是通过直接引用,就像下面这个例子一样:

 
   
   
 
  1. import { ModuleA } from './module-A';

  2. import { ModuleB } from './module-B';


  3. class ModuleC {

  4. constructor() {

  5. this.a = new ModuleA();

  6. this.b = new ModuleB();

  7. }

  8. }

这么做会造成 ModuleC依赖于 ModuleAModuleB,产生了模块间的耦合。为了解决模块间的强耦合性, IoC的概念就产生了。

我们通过使用一个容器来管理我们的模块,这样模块之间的耦合性就降低了(下面这个例子只是模仿 IoC 的过程,Container 需要另外实现):

 
   
   
 
  1. // container.js

  2. import { ModuleA } from './module-A';

  3. import { ModuleB } from './module-B';


  4. // Container是我们假设的一个模块容器

  5. export const container = new Container();

  6. container.bindModule(ModuleA);

  7. container.bindModule(ModuleB);


  8. // ModuleC.js

  9. import { container } from './container';

  10. class ModuleC {

  11. constructor() {

  12. this.a = container.getModule('ModuleA');

  13. this.b = container.getModule('ModuleB');

  14. }

  15. }

为了让大家更清楚 IoC 的过程,我举一个例子,方便大家理解。

当我要找工作的时候,我会去网上搜索想要的工作岗位,然后去投递简历,这个过程叫做控制正转,也就是说控制权在我的手上。而对于控制反转,找工作的过程就变成了,我把简历上传到拉钩这样的第三方平台(容器),第三方平台负责管理很多人的简历。此时HR(其他模块)如果想要招人,就会按照条件在第三方平台查询到我,然后再联系安排面试。

什么是 DI?

DI 英文全称为 Dependency Injection,即依赖注入。依赖注入是控制反转最常见的一种应用方式,即通过控制反转,在对象创建的时候,自动注入一些依赖对象。

如何使用 TypeScript 实现依赖注入?

NestjsAngular中,我们需要通过装饰器 @Injectable()让我们依赖注入到类实例中。而理解他们如何实现依赖注入,我们需要先对装饰器有所了解。下面我们简单的介绍一下什么是装饰器。

装饰器(Decorator)

TypeScript中的装饰器是基于 ECMAScript标准的,而装饰器提案仍处于stage2,存在很多不稳定因素,而且API在未来可能会出现破坏性的更改,所以该特性在TS中仍是一个实验性特性,默认是不启用的(后面将会介绍如何配置开启)。

装饰器定义

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符(getter, setter),属性或参数上。装饰器采用 @expression这种形式进行使用。

下面是使用装饰器的一个简单例子:

 
   
   
 
  1. function demo(target) {

  2. // 在这里装饰target

  3. }


  4. @demo

  5. class DemoClass {}

装饰器工厂

如果我们需要定制装饰器,这个时候就需要一个工厂函数,返回一个装饰器,使用过程如下所示:

 
   
   
 
  1. function decoratorFactory(value: string) {

  2. return function(target) {

  3. target.value = value;

  4. };

  5. }

装饰器组合

如果需要同时使用多个装饰器,可以使用 @f@gx这种语法。

类装饰器

类装饰器是声明在类定义之前,可以用来监视、修改或替换类定义。类装饰器接收的参数就是类本身。

 
   
   
 
  1. function addDemo(target) {

  2. // 此处的target就是DemoClass

  3. target.demo = 'demo';

  4. }


  5. @addDemo

  6. class DemoClass {}

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

装饰器运行时会被当做函数执行,方法和访问器接收下面三个参数:

  1. 对于静态属性来说是类的构造函数(Constructor),对于实例属性是类的原型对象(Prototype)。

  2. 属性(方法、属性、访问器)的名字。

  3. 属性的属性描述符(详情查看这个文档)。

特别地,对于属性装饰器只接收 1 和 2 这两个参数,没有第3个参数的原因是因为无法在定义原型对象时,描述实例上的属性。

通过下面这个例子,我们可以具体看一下这三个参数是什么,方便大家理解:

 
   
   
 
  1. function decorator(target: any, key: string, descriptor: PropertyDescriptor) {}


  2. class Demo {

  3. // target -> Demo.prototype

  4. // key -> 'demo1'

  5. // descriptor -> undefined

  6. @decorator

  7. demo1: string;


  8. // target -> Demo

  9. // key -> 'demo2'

  10. // descriptor -> PropertyDescriptor类型

  11. @decorator

  12. static demo2: string = 'demo2';


  13. // target -> Demo.prototype

  14. // key -> 'demo3'

  15. // descriptor -> PropertyDescriptor类型

  16. @decorator

  17. get demo3() {

  18. return 'demo3';

  19. }


  20. // target -> Demo.prototype

  21. // key -> 'method'

  22. // descriptor -> PropertyDescriptor类型

  23. method() {}

  24. }

参数装饰器

参数装饰器声明在一个参数声明之前。运行时当做函数被调用,这个函数接收下面三个参数:

  1. 对于静态属性来说是类的构造函数,对于实例属性是类的原型对象。

  2. 属性(函数)的名字。

  3. 参数在函数参数列表中的索引。

 
   
   
 
  1. function parameterDecorator(

  2. target: Object,

  3. key: string | symbol,

  4. index: number

  5. ){}


  6. class Demo {

  7. // target -> Demo.prototype

  8. // key -> 'demo1'

  9. // index -> 0

  10. demo1(@parameterDecorator param1: string) {

  11. return param1;

  12. }

  13. }

TypeScript中的元数据(Metadata)

注意:元数据是 Angular 以及 Nestjs 依赖注入实现的基础,请务必看完本章节。

因为 Decorators是实验性特性,所以如果想要支持装饰器功能,需要在 tsconfig.json中添加以下配置。

 
   
   
 
  1. {

  2. "compilerOptions": {

  3. "experimentalDecorators": true,

  4. "emitDecoratorMetadata": true

  5. }

  6. }

使用元数据需要安装并引入 reflect-metadata这个库。这样在编译后的 js 文件中,就可以通过元数据获取类型信息。

 
   
   
 
  1. // 引入reflect-metadata

  2. import 'reflect-metadata';

你们应该会比较好奇,运行时JS是如何获取类型信息的呢?请紧张地继续往下看:

引入了 reflect-metadata后,我们就可以使用其封装在 Reflect上的相关接口,具体请查看其文档。然后在装饰器函数中可以通过下列三种 metadataKey获取类型信息。

  • design:type: 属性类型

  • design:paramtypes: 参数类型

  • design:returntype: 返回值类型

具体可以看下面的例子(每种类型的值都写在注释里了):

 
   
   
 
  1. const classDecorator = (target: Object) => {

  2. console.log(Reflect.getMetadata('design:paramtypes', target));

  3. };


  4. const propertyDecorator = (target: Object, key: string | symbol) => {

  5. console.log(Reflect.getMetadata('design:type', target, key));

  6. console.log(Reflect.getMetadata('design:paramtypes', target, key));

  7. console.log(Reflect.getMetadata('design:returntype', target, key));

  8. };


  9. // paramtypes -> [String] 即构造函数接收的参数

  10. @classDecorator

  11. class Demo {

  12. innerValue: string;

  13. constructor(val: string) {

  14. this.innerValue = val;

  15. }


  16. /*

  17. * 元数据的值如下:

  18. * type -> String

  19. * paramtypes -> undefined

  20. * returntype -> undefined

  21. */

  22. @propertyDecorator

  23. demo1: string = 'demo1';


  24. /*

  25. * 元数据的值如下:

  26. * type -> Function

  27. * paramtypes -> [String]

  28. * returntype -> String

  29. */

  30. @propertyDecorator

  31. demo2(str: string): string {

  32. return str;

  33. }

  34. }

上面的代码执行之后的返回如下所示:

 
   
   
 
  1. [Function: Function] [ [Function: String] ] [Function: String]

  2. [Function: String] undefined undefined

  3. [ [Function: String] ]

我列出了各种装饰器含有的元数据类型(即不是undefined的类型):

  • 类装饰器: design:paramtypes

  • 属性装饰器: design:type

  • 参数装饰器、方法装饰器: design:type、 design:paramtypes、 design:returntype

  • 访问器装饰器: design:type、 design:paramtypes

依赖注入(DI)

说了那么久,终于讲到了本篇文档最为关键的内容了

以上是关于基于 TypeScript 的 IoC 与 DI的主要内容,如果未能解决你的问题,请参考以下文章

Spring的Ioc与DI

Unity IOC/DI使用

IOC容器初始化

Spring IoC基于XML的DI详细配置全解两万字

Spring_IOC控制反转和DI依赖注入

spring的IOC与DI