基于 TypeScript 的 IoC 和 DI

栏目: 后端 · 发布时间: 4年前

内容简介:在使用上述代码中使用了你可能会比较好奇,

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

import { Component } from '@angular/core';
import { OtherService } from './other.service.ts';

@Component({
    // 组件属性
})
export class AppComponent {
  constructor(public otherService: OtherService) {
    // 为什么这里的otherService会被自动传入
  }
}
复制代码

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

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

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

什么是 IoC?

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

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

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

class ModuleC {
  constructor() {
    this.a = new ModuleA();
    this.b = new ModuleB();
  }
}
复制代码

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

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

// container.js
import { ModuleA } from './module-A';
import { ModuleB } from './module-B';

// Container是我们假设的一个模块容器
export const container = new Container();
container.bindModule(ModuleA);
container.bindModule(ModuleB);

// ModuleC.js
import { container } from './container';
class ModuleC {
  constructor() {
    this.a = container.getModule('ModuleA');
    this.b = container.getModule('ModuleB');
  }
}
复制代码

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

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

什么是 DI?

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

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

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

装饰器(Decorator)

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

装饰器定义

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

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

function demo(target) {
  // 在这里装饰target
}

@demo
class DemoClass {}
复制代码

装饰器工厂

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

function decoratorFactory(value: string) {
  return function(target) {
    target.value = value;
  };
}
复制代码

装饰器组合

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

类装饰器

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

function addDemo(target) {
  // 此处的target就是DemoClass
  target.demo = 'demo';
}

@addDemo
class DemoClass {}
复制代码

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

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

  1. 对于静态属性来说是类的构造函数(Constructor),对于实例属性是类的原型对象(Prototype)。
  2. 属性(方法、属性、访问器)的名字。
  3. 属性的属性描述符(详情查看这个文档)。

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

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

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

class Demo {
  // target -> Demo.prototype
  // key -> 'demo1'
  // descriptor -> undefined
  @decorator
  demo1: string;

  // target -> Demo
  // key -> 'demo2'
  // descriptor -> PropertyDescriptor类型
  @decorator
  static demo2: string = 'demo2';

  // target -> Demo.prototype
  // key -> 'demo3'
  // descriptor -> PropertyDescriptor类型
  @decorator
  get demo3() {
    return 'demo3';
  }
  
  // target -> Demo.prototype
  // key -> 'method'
  // descriptor -> PropertyDescriptor类型
  method() {}
}
复制代码

参数装饰器

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

  1. 对于静态属性来说是类的构造函数,对于实例属性是类的原型对象。
  2. 属性(函数)的名字。
  3. 参数在函数参数列表中的索引。
function parameterDecorator(
  target: Object,
  key: string | symbol,
  index: number
) {}

class Demo {
  // target -> Demo.prototype
  // key -> 'demo1'
  // index -> 0
  demo1(@parameterDecorator param1: string) {
    return param1;
  }
}
复制代码

TypeScript中的元数据(Metadata)

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

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

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
复制代码

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

// 引入reflect-metadata
import 'reflect-metadata';
复制代码

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

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

design:type
design:paramtypes
design:returntype

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

const classDecorator = (target: Object) => {
  console.log(Reflect.getMetadata('design:paramtypes', target));
};

const propertyDecorator = (target: Object, key: string | symbol) => {
  console.log(Reflect.getMetadata('design:type', target, key));
  console.log(Reflect.getMetadata('design:paramtypes', target, key));
  console.log(Reflect.getMetadata('design:returntype', target, key));
};

// paramtypes -> [String] 即构造函数接收的参数
@classDecorator
class Demo {
  innerValue: string;
  constructor(val: string) {
    this.innerValue = val;
  }

  /*
   * 元数据的值如下:
   * type -> String
   * paramtypes -> undefined
   * returntype -> undefined
   */
  @propertyDecorator
  demo1: string = 'demo1';

  /*
   * 元数据的值如下:
   * type -> Function
   * paramtypes -> [String]
   * returntype -> String
   */
  @propertyDecorator
  demo2(str: string): string {
    return str;
  }
}
复制代码

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

[Function: Function] [ [Function: String] ] [Function: String]
[Function: String] undefined undefined
[ [Function: String] ]
复制代码

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

  • 类装饰器: design:paramtypes
  • 属性装饰器: design:type
  • 参数装饰器、方法装饰器: design:typedesign:paramtypesdesign:returntype
  • 访问器装饰器: design:typedesign:paramtypes

依赖注入(DI)

说了那么久,终于讲到了本篇文档最为关键的内容了:tada:,本节的实现请确保元数据在你的TS代码中是可用的。

下面我给出一个简单的实现依赖注入的 TS 实例:

// 构造函数类型
type Constructor<T = any> = new (...arg: any[]) => T;

// 类装饰器,用于标识类是需要注入的
const Injectable = (): ClassDecorator => target => {};

// 需要注入的类
class InjectService {
  a = 'inject';
}

// 被注入的类
@Injectable()
class DemoService {
  constructor(public injectService: InjectService) {}

  test() {
    console.log(this.injectService.a);
  }
}

// 依赖注入函数Factory
const Factory = <T>(target: Constructor<T>): T => {
  // 获取target类的构造函数参数providers
  const providers = Reflect.getMetadata('design:paramtypes', target);
  // 将参数依次实例化
  const args = providers.map((provider: Constructor) => new provider());
  // 将实例化的数组作为target类的参数,并返回target的实例
  return new target(...args);
};

Factory(DemoService).test(); // inject
复制代码

通过上述代码中的 Factory ,我们就成功地将 InjectService 注入到 DemoService 中。

我们先看一下上面的代码中 DemoService 编译成 JS 之后的样子:

// 此处省略了__decorate和__metadata的实现代码
var DemoService = /** @class */ (function() {
  function DemoService(injectService) {
    this.injectService = injectService;
  }
  DemoService.prototype.test = function() {
    console.log(this.injectService.a);
  };
  DemoService = __decorate(
    [Injectable(), __metadata('design:paramtypes', [InjectService])],
    DemoService
  );
  return DemoService;
})();
复制代码

从上面的代码中,我们看到 TS 将构造函数的参数类型 [InjectService] ,通过元数据存储了起来。所以在依赖注入的时候,我们就可以通过 Reflect.getMetadata('design:paramtypes', target) 取出了这个参数,并将其实例化后赋值到 this.injectService 中,这样一个简单的依赖注入就完成了。

如果你发现本文中有错误或者不合适的地方,欢迎留言反馈。

参考文献


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

大数据预测

大数据预测

【美】埃里克·西格尔 / 周昕 / 中信出版社 / 2014-3 / 58.00

360公司董事长周鸿祎、《罗辑思维》主讲人罗振宇郑重推荐 2020年的一天,在你驱车前往公司的路上,导航系统通过预测交通流量,会自动帮你选择一条最合适的交通路线;车内推荐系统会根据你的饮食习惯预测你可能会喜欢吃什么,并推荐沿途的早餐店;你的电子社交助理已经为你自动选择了你可能感兴趣的社交网信息;当车内系统预测到你驾车有些分心时,座椅会自动震动进行提醒…… 以上这些情景不是科幻大片独有的......一起来看看 《大数据预测》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器