基于 TypeScript 的 IoC 和 DI

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

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

在使用 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 中,这样一个简单的依赖注入就完成了。

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

参考文献


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

查看所有标签

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

Effective JavaScript

Effective JavaScript

赫尔曼 (David Herman) / 黄博文、喻杨 / 机械工业出版社 / 2014-1-1 / CNY 49.00

Effective 系列丛书经典著作,亚马逊五星级畅销书,Ecma 的JavaScript 标准化委员会著名专家撰写,JavaScript 语言之父、Mozilla CTO —— Brendan Eich 作序鼎力推荐!作者凭借多年标准化委员会工作和实践经验,深刻辨析JavaScript 的内部运作机制、特性、陷阱和编程最佳实践,将它们高度浓缩为极具实践指导意义的 68 条精华建议。 本书共......一起来看看 《Effective JavaScript》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

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

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器