关于前端:了不起的-IoC-与-DI

4次阅读

共计 23298 个字符,预计需要花费 59 分钟才能阅读完成。

本文阿宝哥将从六个方面动手,全方位带你一起摸索面向对象编程中 IoC(管制反转)和 DI(依赖注入) 的设计思维。浏览完本文,你将理解以下内容:

  • IoC 是什么、IoC 能解决什么问题;
  • IoC 与 DI 之间的关系、未应用 DI 框架和应用 DI 框架之间的区别;
  • DI 在 AngularJS/Angular 和 NestJS 中的利用;
  • 理解如何应用 TypeScript 实现一个 IoC 容器,并理解 装璜器、反射 的相干常识。

一、背景概述

在介绍什么是 IoC 容器之前,阿宝哥来举一个日常工作中很常见的场景,即创立指定类的实例。最简略的情景是该类没有依赖其余类,但事实往往是残暴的,咱们在创立某个类的实例时,须要依赖不同类对应的实例。为了让小伙伴们可能更好地了解上述的内容,阿宝哥来举一个例子。

一辆小汽车 ???? 通常由 发动机、底盘、车身和电气设备 四大局部组成。汽车电气设备的外部结构很简单,简略起见,咱们只思考三个局部:发动机、底盘和车身。

(图片起源:https://www.newkidscar.com/ve…)

在现实生活中,要造辆车还是很艰难的。而在软件的世界中,这可难不倒咱们。???? 是阿宝哥要造的车子,有木有很酷。

(图片起源:https://pixabay.com/zh/illust…)

在开始造车前,咱们得先看一下“图纸”:

看完下面的“图纸”,咱们马上来开启造车之旅。第一步咱们先来定义车身类:

1. 定义车身类

export default class Body {}

2. 定义底盘类

export default class Chassis {}

3. 定义引擎类

export default class Engine {start() {console.log("引擎动员了");
  }
}

4. 定义汽车类

import Engine from './engine';
import Chassis from './chassis';
import Body from './body';

export default class Car {
    engine: Engine;
    chassis: Chassis;
    body: Body;

    constructor() {this.engine = new Engine();
      this.body = new Body();
      this.chassis = new Chassis();}

    run() {this.engine.start();
    }
}

所有已准备就绪,咱们马上来造一辆车:

const car = new Car(); // 阿宝哥造辆新车
car.run(); // 控制台输入:引擎动员了 

当初尽管车曾经能够启动了,但却存在以下问题:

  • 问题一:在造车的时候,你不能抉择配置。比方你想更换汽车引擎的话,依照目前的计划,是实现不了的。
  • 问题二:在汽车类外部,你须要在构造函数中手动去创立汽车的各个部件。

为了解决第一个问题,提供更灵便的计划,咱们能够重构一下已定义的汽车类,具体如下:

export default class Car {
    body: Body;
    engine: Engine;
    chassis: Chassis;
  
    constructor(engine, body, chassis) {
      this.engine = engine;
      this.body = body;
      this.chassis = chassis;
    }

    run() {this.engine.start();
    }
}

重构完汽车类,咱们来从新造辆新车:

const engine = new NewEngine();
const body = new Body();
const chassis = new Chassis();

const newCar = new Car(engine, body, chassis);
newCar.run();

此时咱们曾经解决了下面提到的第一个问题,要解决第二个问题咱们要来理解一下 IoC(管制反转)的概念。

二、IoC 是什么

IoC(Inversion of Control),即“管制反转”。在开发中,IoC 意味着你设计好的对象交给容器管制,而不是应用传统的形式,在对象外部间接管制。

如何了解好 IoC 呢?了解好 IoC 的要害是要明确 “谁管制谁,管制什么,为何是反转,哪些方面反转了”,咱们来深入分析一下。

  • 谁管制谁,管制什么:在传统的程序设计中,咱们间接在对象外部通过 new 的形式创建对象,是程序被动创立依赖对象; 而 IoC 是有专门一个容器来创立这些对象,即由 IoC 容器管制对象的创立

    谁管制谁?当然是 IoC 容器管制了对象;管制什么?次要是管制内部资源(依赖对象)获取。

  • 为何是反转了,哪些方面反转了:有反转就有正转,传统应用程序是由咱们本人在程序中被动管制去获取依赖对象,也就是正转; 而反转则是由容器来帮忙创立及注入依赖对象

    为何是反转?因为由容器帮咱们查找及注入依赖对象,对象只是被动的承受依赖对象,所以是反转了;哪些方面反转了?依赖对象的获取被反转了。

三、IoC 能做什么

IoC 不是一种技术,只是一种思维,是面向对象编程中的一种设计准则,能够用来减低计算机代码之间的耦合度。

传统应用程序都是由咱们在类外部被动创立依赖对象,从而导致类与类之间高耦合,难于测试; 有了 IoC 容器后,把创立和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是涣散耦合。 这样也便于测试,利于性能复用,更重要的是使得程序的整个体系结构变得非常灵活。

其实 IoC 对编程带来的最大扭转不是从代码上,而是思维上,产生了“主从换位”的变动。应用程序原本是老大,要获取什么资源都是主动出击,但在 IoC 思维中,应用程序就变成被动了,被动的期待 IoC 容器来创立并注入它所需的资源了。

四、IoC 与 DI 之间的关系

对于管制反转来说,其中最常见的形式叫做 依赖注入 ,简称为 DI(Dependency Injection)。

组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动静的将某个依赖关系注入到组件之中。 依赖注入的目标并非为软件系统带来更多功能,而是为了晋升组件重用的频率,并为零碎搭建一个灵便、可扩大的平台。

通过依赖注入机制,咱们只须要通过简略的配置,而无需任何代码就可指定指标须要的资源,实现本身的业务逻辑,而不须要关怀具体的资源来自何处,由谁实现。

了解 DI 的要害是 “谁依赖了谁,为什么须要依赖,谁注入了谁,注入了什么”

  • 谁依赖了谁:当然是应用程序依赖 IoC 容器;
  • 为什么须要依赖:应用程序须要 IoC 容器来提供对象须要的内部资源(包含对象、资源、常量数据);
  • 谁注入谁:很显著是 IoC 容器注入应用程序依赖的对象;
  • 注入了什么:注入某个对象所需的内部资源(包含对象、资源、常量数据)。

那么 IoC 和 DI 有什么关系?其实它们是同一个概念的不同角度形容,因为管制反转的概念比拟含混(可能只是了解为容器管制对象这一个层面,很难让人想到谁来保护依赖关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:“依赖注入”,绝对 IoC 而言,“依赖注入”明确形容了被注入对象依赖 IoC 容器配置依赖对象

总的来说,管制反转(Inversion of Control)是说创建对象的控制权产生转移,以前创建对象的主动权和创立机会由应用程序把控,而当初这种权力转交给 IoC 容器,它就是一个专门用来创建对象的工厂,你须要什么对象,它就给你什么对象。 有了 IoC 容器,依赖关系就扭转了,原先的依赖关系就没了,它们都依赖 IoC 容器了,通过 IoC 容器来建设它们之间的关系

后面介绍了那么多的概念,当初咱们来看一下未应用依赖注入框架和应用依赖注入框架之间有什么显著的区别。

4.1 未应用依赖注入框架

假如咱们的服务 A 依赖于服务 B,即要应用服务 A 前,咱们须要先创立服务 B。具体的流程如下图所示:

从上图可知,未应用依赖注入框架时,服务的使用者须要关怀服务自身和其依赖的对象是如何创立的,且须要手动保护依赖关系。若服务自身须要依赖多个对象,这样就会减少应用难度和前期的保护老本。对于上述的问题,咱们能够思考引入依赖注入框架。上面咱们来看一下引入依赖注入框架,整体流程会产生什么变动。

4.2 应用依赖注入框架

应用依赖注入框架之后,零碎中的服务会对立注册到 IoC 容器中,如果服务有依赖其余服务时,也须要对依赖进行申明。当用户须要应用特定的服务时,IoC 容器会负责该服务及其依赖对象的创立与管理工作。具体的流程如下图所示:

到这里咱们曾经介绍了 IoC 与 DI 的概念及特点,接下来咱们来介绍 DI 的利用。

五、DI 的利用

DI 在前端和服务端都有相应的利用,比方在前端畛域的代表是 AngularJS 和 Angular,而在服务端畛域是 Node.js 生态中比拟闻名的 NestJS。接下来阿宝哥将简略介绍一下 DI 在 AngularJS/Angular 和 NestJS 中的利用。

5.1 DI 在 AngularJS 中的利用

在 AngularJS 中,依赖注入是其外围的个性之一。在 AngularJS 中申明依赖项有 3 种形式:

// 形式一:应用 $inject annotation 形式
let fn = function (a, b) {};
fn.$inject = ['a', 'b'];

// 形式二:应用 array-style annotations 形式
let fn = ['a', 'b', function (a, b) {}];

// 形式三:应用隐式申明形式 
let fn = function (a, b) {}; // 不举荐 

对于以上的代码,置信应用过 AngularJS 的小伙们都不会生疏。作为 AngularJS 外围性能个性的 DI 还是蛮弱小的,但随着 AngularJS 的遍及和利用的复杂度一直进步,AngularJS DI 零碎的问题就裸露进去了。

这里阿宝哥简略介绍一下 AngularJS DI 零碎存在的几个问题:

  • 外部缓存:AngularJS 应用程序中所有的依赖项都是单例,咱们不能管制是否应用新的实例;
  • 命名空间抵触:在零碎中咱们应用字符串来标识服务的名称,假如咱们在我的项目中已有一个 CarService,然而第三方库中也引入了同样的服务,这样的话就容易呈现混同。

因为 AngularJS DI 存在以上的问题,所以在后续的 Angular 从新设计了新的 DI 零碎。

5.2 DI 在 Angular 中的利用

以后面汽车的例子为例,咱们能够把汽车、发动机、底盘和车身这些认为是一种“服务”,所以它们会以服务提供者的模式注册到 DI 零碎中。为了能辨别不同服务,咱们须要应用不同的令牌(Token)来标识它们。接着咱们会基于已注册的服务提供者创立注入器对象。

之后,当咱们须要获取指定服务时,咱们就能够通过该服务对应的令牌,从注入器对象中获取令牌对应的依赖对象。上述的流程的具体如下图所示:

好的,理解完上述的流程。上面咱们来看一下如何应用 Angular 内置的 DI 零碎来“造车”。

5.2.1 car.ts
// car.ts
import {Injectable, ReflectiveInjector} from '@angular/core';

// 配置 Provider
@Injectable({providedIn: 'root',})
export class Body {}

@Injectable({providedIn: 'root',})
export class Chassis {}

@Injectable({providedIn: 'root',})
export class Engine {start() {console.log('引擎动员了');
  }
}

@Injectable()
export default class Car {
  // 应用结构注入形式注入依赖对象
  constructor(
    private engine: Engine,
    private body: Body,
    private chassis: Chassis
  ) {}

  run() {this.engine.start();
  }
}

const injector = ReflectiveInjector.resolveAndCreate([
  Car,
  Engine,
  Chassis,
  Body,
]);

const car = injector.get(Car);
car.run();

在以上代码中咱们调用 ReflectiveInjector 对象的 resolveAndCreate 办法手动创立注入器,而后依据车辆对应的 Token 来获取对应的依赖对象。通过观察上述代码,你能够发现,咱们曾经不须要手动地治理和保护依赖对象了,这些“脏活”、“累活”曾经交给注入器来解决了。

此外,如果要能失常获取汽车对象,咱们还须要在 app.module.ts 文件中申明 Car 对应 Provider,具体如下所示:

5.2.2 app.module.ts
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

import {AppComponent} from './app.component';
import Car, {Body, Chassis, Engine} from './car';

@NgModule({declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [{provide: Car, deps: [Engine, Body, Chassis] }],
  bootstrap: [AppComponent],
})
export class AppModule {}

5.3 DI 在 NestJS 中的利用

NestJS 是构建高效,可扩大的 Node.js Web 应用程序的框架。它应用古代的 JavaScript 或 TypeScript(保留与纯 JavaScript 的兼容性),并联合 OOP(面向对象编程),FP(函数式编程)和 FRP(函数响应式编程)的元素。

在底层,Nest 应用了 Express,但也提供了与其余各种库的兼容,例如 Fastify,能够不便地应用各种可用的第三方插件。

近几年,因为 Node.js,JavaScript 曾经成为 Web 前端和后端应用程序的「通用语言」,从而产生了像 Angular、React、Vue 等令人耳目一新的我的项目,这些我的项目进步了开发人员的生产力,使得能够疾速构建可测试的且可扩大的前端应用程序。然而,在服务器端,尽管有很多优良的库、helper 和 Node 工具,然而它们都没有无效地解决次要问题 —— 架构。

NestJS 旨在提供一个开箱即用的应用程序体系结构,容许轻松创立高度可测试,可扩大,涣散耦合且易于保护的应用程序。 在 NestJS 中也为咱们开发者提供了依赖注入的性能,这里咱们以官网的示例来演示一下依赖注入的性能。

5.3.1 app.service.ts
import {Injectable} from '@nestjs/common';

@Injectable()
export class AppService {getHello(): string {return 'Hello World!';}
}
5.3.2 app.controller.ts
import {Get, Controller, Render} from '@nestjs/common';
import {AppService} from './app.service';

@Controller()
export class AppController {constructor(private readonly appService: AppService) {}

  @Get()
  @Render('index')
  render() {const message = this.appService.getHello();
    return {message};
  }
}

在 AppController 中,咱们通过结构注入的形式注入了 AppService 对象,当用户拜访首页的时候,咱们会调用 AppService 对象的 getHello 办法来获取 'Hello World!' 音讯,并把音讯返回给用户。当然为了保障依赖注入能够失常工作,咱们还须要在 AppModule 中申明 providers 和 controllers,具体操作如下:

import {Module} from '@nestjs/common';
import {AppController} from './app.controller';
import {AppService} from './app.service';

@Module({imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

其实 DI 并不是 AngularJS/Angular 和 NestJS 所特有的,如果你想在其余我的项目中应用 DI/IoC 的性能个性,阿宝哥举荐你应用 InversifyJS,它是一个可用于 JavaScript 和 Node.js 利用,功能强大、轻量的 IoC 容器。

对 InversifyJS 感兴趣的小伙伴能够自行理解一下,阿宝哥就不持续开展介绍了。接下来,咱们将进入本文的重点,即介绍如何应用 TypeScript 实现一个简略的 IoC 容器,该容器实现的性能如下图所示:

六、手写 IoC 容器

为了让大家能更好地了解 IoC 容器的实现代码,阿宝哥来介绍一些相干的前置常识。

6.1 装璜器

如果你有应用过 Angular 或 NestJS,置信你对以下的代码不会生疏。

@Injectable()
export class HttpService {
  constructor(private httpClient: HttpClient) {}}

在以上代码中,咱们应用了 Injectable 装璜器。该装璜器用于示意此类能够主动注入其依赖项。其中 @Injectable() 中的 @ 符号属于语法糖。

装璜器是一个包装类,函数或办法并为其增加行为的函数。这对于定义与对象关联的元数据很有用。装璜器有以下四种分类:

  • 类装璜器(Class decorators)
  • 属性装璜器(Property decorators)
  • 办法装璜器(Method decorators)
  • 参数装璜器(Parameter decorators)

后面示例中应用的 @Injectable() 装璜器,属于类装璜器。在该类装璜器润饰的 HttpService 类中,咱们通过结构注入的形式注入了用于解决 HTTP 申请的 HttpClient 依赖对象。

6.2 反射

@Injectable()
export class HttpService {
  constructor(private httpClient: HttpClient) {}}

以上代码若设置编译的指标为 ES5,则会生成以下代码:

// 疏忽__decorate 函数等代码
var __metadata = (this && this.__metadata) || function (k, v) {if (typeof Reflect === "object" && typeof Reflect.metadata === "function") 
      return Reflect.metadata(k, v);
};

var HttpService = /** @class */ (function () {function HttpService(httpClient) {this.httpClient = httpClient;}
    var _a;
    HttpService = __decorate([Injectable(),
        __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient)
           === "function" ? _a : Object])
    ], HttpService);
    return HttpService;
}());

通过观察上述代码,你会发现 HttpService 构造函数中 httpClient 参数的类型被擦除了,这是因为 JavaScript 是弱类型语言。那么如何在运行时,保障注入正确类型的依赖对象呢?这里 TypeScript 应用 reflect-metadata 这个第三方库来存储额定的类型信息。

reflect-metadata 这个库提供了很多 API 用于操作元信息,这里咱们只简略介绍几个罕用的 API:

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// get metadata value of a metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// apply metadata via a decorator to a constructor
@Reflect.metadata(metadataKey, metadataValue)
class C {// apply metadata via a decorator to a method (property)
  @Reflect.metadata(metadataKey, metadataValue)
  method() {}
}

对于上述的 API 只需简略理解一下即可。在后续的内容中,咱们将介绍具体如何应用。这里咱们须要留神以下两个问题:

  • 对于类或函数,咱们须要应用装璜器来润饰它们,这样能力保留元数据。
  • 只有类、枚举或原始数据类型能被记录。接口和联结类型作为“对象”呈现。这是因为这些类型在编译后齐全隐没,而类却始终存在。

6.3 定义 Token 和 Provider

理解完装璜器与反射相干的基础知识,接下来咱们来开始实现 IoC 容器。咱们的 IoC 容器将应用两个次要的概念:令牌(Token)和提供者(Provider)。令牌是 IoC 容器所要创建对象的标识符,而提供者用于形容如何创立这些对象。

IoC 容器最小的公共接口如下所示:

export class Container {addProvider<T>(provider: Provider<T>) {} // TODO
  inject<T>(type: Token<T>): T {} // TODO}

接下来咱们先来定义 Token:

// type.ts
interface Type<T> extends Function {new (...args: any[]): T;
}

// provider.ts
class InjectionToken {constructor(public injectionIdentifier: string) {}}

type Token<T> = Type<T> | InjectionToken;

Token 类型是一个联结类型,既能够是一个函数类型也能够是 InjectionToken 类型。AngularJS 中应用字符串作为 Token,在某些状况下,可能会导致抵触。因而,为了解决这个问题,咱们定义了 InjectionToken 类,来避免出现命名抵触问题。

定义完 Token 类型,接下来咱们来定义三种不同类型的 Provider:

  • ClassProvider:提供一个类,用于创立依赖对象;
  • ValueProvider:提供一个已存在的值,作为依赖对象;
  • FactoryProvider:提供一个工厂办法,用于创立依赖对象。
// provider.ts
export type Factory<T> = () => T;

export interface BaseProvider<T> {provide: Token<T>;}

export interface ClassProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useClass: Type<T>;
}

export interface ValueProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useValue: T;
}

export interface FactoryProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useFactory: Factory<T>;
}

export type Provider<T> =
  | ClassProvider<T>
  | ValueProvider<T>
  | FactoryProvider<T>;

为了更不便的辨别这三种不同类型的 Provider,咱们自定义了三个类型守卫函数:

// provider.ts
export function isClassProvider<T>(provider: BaseProvider<T>): provider is ClassProvider<T> {return (provider as any).useClass !== undefined;
}

export function isValueProvider<T>(provider: BaseProvider<T>): provider is ValueProvider<T> {return (provider as any).useValue !== undefined;
}

export function isFactoryProvider<T>(provider: BaseProvider<T>): provider is FactoryProvider<T> {return (provider as any).useFactory !== undefined;
}

6.4 定义装璜器

在后面咱们曾经提过了,对于类或函数,咱们须要应用装璜器来润饰它们,这样能力保留元数据。因而,接下来咱们来别离创立 InjectableInject 装璜器。

6.4.1 Injectable 装璜器

Injectable 装璜器用于示意此类能够主动注入其依赖项,该装璜器属于类装璜器。在 TypeScript 中,类装璜器的申明如下:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) 
  => TFunction | void;

类装璜器顾名思义,就是用来装璜类的。它接管一个参数:target: TFunction,示意被装璜的类。上面咱们来看一下 Injectable 装璜器的具体实现:

// Injectable.ts
import {Type} from "./type";
import "reflect-metadata";

const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY");

export function Injectable() {return function(target: any) {Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    return target;
  };
}

在以上代码中,当调用完 Injectable 函数之后,会返回一个新的函数。在新的函数中,咱们应用 reflect-metadata 这个库提供的 defineMetadata API 来保留元信息,其中 defineMetadata API 的应用形式如下所示:

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

Injectable 类装璜器应用形式也简略,只须要在被装璜类的上方应用 @Injectable() 语法糖就能够利用该装璜器:

@Injectable()
export class HttpService {
  constructor(private httpClient: HttpClient) {}}

在以上示例中,咱们注入的是 Type 类型的 HttpClient 对象。但在理论的我的项目中,往往会比较复杂。除了须要注入 Type 类型的依赖对象之外,咱们还可能会注入其余类型的依赖对象,比方咱们心愿在 HttpService 服务中注入近程服务器的 API 地址。针对这种情景,咱们须要应用 Inject 装璜器。

6.4.2 Inject 装璜器

接下来咱们来创立 Inject 装璜器,该装璜器属于参数装璜器。在 TypeScript 中,参数装璜器的申明如下:

declare type ParameterDecorator = (target: Object, 
  propertyKey: string | symbol, parameterIndex: number ) => void

参数装璜器顾名思义,是用来装璜函数参数,它接管三个参数:

  • target: Object —— 被装璜的类;
  • propertyKey: string | symbol —— 办法名;
  • parameterIndex: number —— 办法中参数的索引值。

上面咱们来看一下 Inject 装璜器的具体实现:

// Inject.ts
import {Token} from './provider';
import 'reflect-metadata';

const INJECT_METADATA_KEY = Symbol('INJECT_KEY');

export function Inject(token: Token<any>) {return function(target: any, _: string | symbol, index: number) {Reflect.defineMetadata(INJECT_METADATA_KEY, token, target, `index-${index}`);
    return target;
  };
}

在以上代码中,当调用完 Inject 函数之后,会返回一个新的函数。在新的函数中,咱们应用 reflect-metadata 这个库提供的 defineMetadata API 来保留参数相干的元信息。这里是保留 index 索引信息和 Token 信息。

定义完 Inject 装璜器,咱们就能够利用它来注入咱们后面所提到的近程服务器的 API 地址,具体的应用形式如下:

const API_URL = new InjectionToken('apiUrl');

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}}

6.5 实现 IoC 容器

目前为止,咱们曾经定义了 Token、Provider、Injectable 和 Inject 装璜器。接下来咱们来实现后面所提到的 IoC 容器的 API:

export class Container {addProvider<T>(provider: Provider<T>) {} // TODO
  inject<T>(type: Token<T>): T {} // TODO}
6.5.1 实现 addProvider 办法

addProvider() 办法的实现很简略,咱们应用 Map 来存储 Token 与 Provider 之间的关系:

export class Container {private providers = new Map<Token<any>, Provider<any>>();

  addProvider<T>(provider: Provider<T>) {this.assertInjectableIfClassProvider(provider);
    this.providers.set(provider.provide, provider);
  }
}

在 addProvider() 办法外部除了把 Token 与 Provider 的对应信息保留到 providers 对象中之外,咱们定义了一个 assertInjectableIfClassProvider 办法,用于确保增加的 ClassProvider 是可注入的。该办法的具体实现如下:

private assertInjectableIfClassProvider<T>(provider: Provider<T>) {if (isClassProvider(provider) && !isInjectable(provider.useClass)) {
    throw new Error(
        `Cannot provide ${this.getTokenName(provider.provide)} using class ${this.getTokenName(provider.useClass)}, ${this.getTokenName(provider.useClass)} isn't injectable`
   );
  }
}

在 assertInjectableIfClassProvider 办法体中,咱们应用了后面曾经介绍的 isClassProvider 类型守卫函数来判断是否为 ClassProvider,如果是的话,会判断该 ClassProvider 是否为可注入的,具体应用的是 isInjectable 函数,该函数的定义如下:

export function isInjectable<T>(target: Type<T>) {return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true;
}

在 isInjectable 函数中,咱们应用 reflect-metadata 这个库提供的 getMetadata API 来获取保留在类中的元信息。为了更好地了解以上代码,咱们来回顾一下后面 Injectable 装璜器:

const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY");

export function Injectable() {return function(target: any) {Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    return target;
  };
}

如果增加的 Provider 是 ClassProvider,但 Provider 对应的类是不可注入的,则会抛出异样。为了让异样音讯更加敌对,也更加直观。咱们定义了一个 getTokenName 办法来获取 Token 对应的名称:

private getTokenName<T>(token: Token<T>) {
  return token instanceof InjectionToken
    ? token.injectionIdentifier
    : token.name;
}

当初咱们曾经实现了 Container 类的 addProvider 办法,这时咱们就能够应用它来增加三种不同类型的 Provider:

const container = new Container();
const input = {x: 200};

class BasicClass {}
// 注册 ClassProvider
container.addProvider({provide: BasicClass, useClass:  BasicClass});
// 注册 ValueProvider
container.addProvider({provide: BasicClass, useValue: input});
// 注册 FactoryProvider
container.addProvider({provide: BasicClass, useFactory: () => input });

须要留神的是,以上示例中注册三种不同类型的 Provider 应用的是同一个 Token 仅是为了演示而已。上面咱们来实现 Container 类中外围的 inject 办法。

6.5.2 实现 inject 办法

在看 inject 办法的具体实现之前,咱们先来看一下该办法所实现的性能:

const container = new Container();
const input = {x: 200};

container.addProvider({provide: BasicClass, useValue: input});
const output = container.inject(BasicClass);
expect(input).toBe(output); // true

察看以上的测试用例可知,Container 类中 inject 办法所实现的性能就是依据 Token 获取与之对应的对象。在后面实现的 addProvider 办法中,咱们把 Token 和该 Token 对应的 Provider 保留在 providers Map 对象中。所以在 inject 办法中,咱们能够先从 providers 对象中获取该 Token 对应的 Provider 对象,而后在依据不同类型的 Provider 来获取其对应的对象。

好的,上面咱们来看一下 inject 办法的具体实现:

inject<T>(type: Token<T>): T {let provider = this.providers.get(type);
  // 解决应用 Injectable 装璜器润饰的类
  if (provider === undefined && !(type instanceof InjectionToken)) {provider = { provide: type, useClass: type};
    this.assertInjectableIfClassProvider(provider);
  }
  return this.injectWithProvider(type, provider);
}

在以上代码中,除了解决失常的流程之外。咱们还解决一个非凡的场景,即没有应用 addProvider 办法注册 Provider,而是应用 Injectable 装璜器来装璜某个类。对于这个非凡场景,咱们会依据传入的 type 参数来创立一个 provider 对象,而后进一步调用 injectWithProvider 办法来创建对象,该办法的具体实现如下:

private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {if (provider === undefined) {throw new Error(`No provider for type ${this.getTokenName(type)}`);
  }
  if (isClassProvider(provider)) {return this.injectClass(provider as ClassProvider<T>);
  } else if (isValueProvider(provider)) {return this.injectValue(provider as ValueProvider<T>);
  } else {return this.injectFactory(provider as FactoryProvider<T>);
  }
 }

injectWithProvider 办法外部,咱们会应用后面定义的用于辨别三种不同类型 Provider 的类型守卫函数来解决不同的 Provider。这里咱们先来看一下最简略 ValueProvider,当发现注入的是 ValueProvider 类型时,则会调用 injectValue 办法来获取其对应的对象:

// {provide: API_URL, useValue: 'https://www.semlinker.com/'}
private injectValue<T>(valueProvider: ValueProvider<T>): T {return valueProvider.useValue;}

接着咱们来看如何解决 FactoryProvider 类型的 Provider,如果发现是 FactoryProvider 类型时,则会调用 injectFactory 办法来获取其对应的对象,该办法的实现也很简略:

// const input = {x: 200};
// container.addProvider({provide: BasicClass, useFactory: () => input });
private injectFactory<T>(valueProvider: FactoryProvider<T>): T {return valueProvider.useFactory();
}

最初咱们来剖析一下如何解决 ClassProvider,对于 ClassProvider 类说,通过 Provider 对象的 useClass 属性,咱们就能够间接获取到类对应的构造函数。最简略的情景是该类没有依赖其余对象,但在大多数场景下,行将实例化的服务类是会依赖其余的对象的。所以在实例化服务类前,咱们须要结构其依赖的对象。

那么当初问题来了,怎么获取类所依赖的对象呢?咱们先来剖析一下以下代码:

const API_URL = new InjectionToken('apiUrl');

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}}

以上代码若设置编译的指标为 ES5,则会生成以下代码:

// 已省略__decorate 函数的定义
var __metadata = (this && this.__metadata) || function (k, v) {if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

var __param = (this && this.__param) || function (paramIndex, decorator) {return function (target, key) {decorator(target, key, paramIndex); }
};

var HttpService = /** @class */ (function () {function HttpService(httpClient, apiUrl) {
        this.httpClient = httpClient;
        this.apiUrl = apiUrl;
    }
    var _a;
    HttpService = __decorate([Injectable(),
        __param(1, Inject(API_URL)),
        __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient) 
          === "function" ? _a : Object, String])
    ], HttpService);
    return HttpService;
}());

察看以上的代码会不会感觉有点晕?不要焦急,阿宝哥会逐个剖析 HttpService 中的两个参数。首先咱们先来剖析 apiUrl 参数:

在图中咱们能够很分明地看到,API_URL 对应的 Token 最终会通过 Reflect.defineMetadata API 进行保留,所应用的 Key 是 Symbol('INJECT_KEY')。而对于另一个参数即 httpClient,它应用的 Key 是 "design:paramtypes",它用于润饰指标对象办法的参数类型。

除了 "design:paramtypes" 之外,还有其余的 metadataKey,比方 design:typedesign:returntype,它们别离用于润饰指标对象的类型和润饰指标对象办法返回值的类型。

由上图可知,HttpService 构造函数的参数类型最终会应用 Reflect.metadata API 进行存储。理解完上述的常识,接下来咱们来定义一个 getInjectedParams 办法,用于获取类构造函数中申明的依赖对象,该办法的具体实现如下:

type InjectableParam = Type<any>;
const REFLECT_PARAMS = "design:paramtypes";

private getInjectedParams<T>(target: Type<T>) {
  // 获取参数的类型
  const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (
      | InjectableParam
      | undefined
  )[];
  if (argTypes === undefined) {return [];
  }
  return argTypes.map((argType, index) => {
    // The reflect-metadata API fails on circular dependencies, and will return undefined
    // for the argument instead.
    if (argType === undefined) {
      throw new Error(`Injection error. Recursive dependency detected in constructor for type ${target.name} 
           with parameter at index ${index}`
      );
    }
    const overrideToken = getInjectionToken(target, index);
    const actualToken = overrideToken === undefined ? argType : overrideToken;
    let provider = this.providers.get(actualToken);
    return this.injectWithProvider(actualToken, provider);
  });
}

因为咱们的 Token 的类型是 Type<T> | InjectionToken 联结类型,所以在 getInjectedParams 办法中咱们也要思考 InjectionToken 的情景,因而咱们定义了一个 getInjectionToken 办法来获取应用 @Inject 装璜器注册的 Token,该办法的实现很简略:

export function getInjectionToken(target: any, index: number) {return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined;
}

当初咱们曾经能够获取类构造函数中所依赖的对象,基于后面定义的 getInjectedParams 办法,咱们就来定义一个 injectClass 办法,用来实例化 ClassProvider 所注册的类。

// {provide: HttpClient, useClass: HttpClient}
private injectClass<T>(classProvider: ClassProvider<T>): T {
  const target = classProvider.useClass;
  const params = this.getInjectedParams(target);
  return Reflect.construct(target, params);
}

这时 IoC 容器中定义的两个办法都曾经实现了,咱们来看一下 IoC 容器的残缺代码:

// container.ts
type InjectableParam = Type<any>;

const REFLECT_PARAMS = "design:paramtypes";

export class Container {private providers = new Map<Token<any>, Provider<any>>();

  addProvider<T>(provider: Provider<T>) {this.assertInjectableIfClassProvider(provider);
    this.providers.set(provider.provide, provider);
  }

  inject<T>(type: Token<T>): T {let provider = this.providers.get(type);
    if (provider === undefined && !(type instanceof InjectionToken)) {provider = { provide: type, useClass: type};
      this.assertInjectableIfClassProvider(provider);
    }
    return this.injectWithProvider(type, provider);
  }

  private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {if (provider === undefined) {throw new Error(`No provider for type ${this.getTokenName(type)}`);
    }
    if (isClassProvider(provider)) {return this.injectClass(provider as ClassProvider<T>);
    } else if (isValueProvider(provider)) {return this.injectValue(provider as ValueProvider<T>);
    } else {
      // Factory provider by process of elimination
      return this.injectFactory(provider as FactoryProvider<T>);
    }
  }

  private assertInjectableIfClassProvider<T>(provider: Provider<T>) {if (isClassProvider(provider) && !isInjectable(provider.useClass)) {
      throw new Error(
        `Cannot provide ${this.getTokenName(provider.provide)} using class ${this.getTokenName(provider.useClass)}, ${this.getTokenName(provider.useClass)} isn't injectable`
      );
    }
  }

  private injectClass<T>(classProvider: ClassProvider<T>): T {
    const target = classProvider.useClass;
    const params = this.getInjectedParams(target);
    return Reflect.construct(target, params);
  }

  private injectValue<T>(valueProvider: ValueProvider<T>): T {return valueProvider.useValue;}

  private injectFactory<T>(valueProvider: FactoryProvider<T>): T {return valueProvider.useFactory();
  }

  private getInjectedParams<T>(target: Type<T>) {const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (
      | InjectableParam
      | undefined
    )[];
    if (argTypes === undefined) {return [];
    }
    return argTypes.map((argType, index) => {
      // The reflect-metadata API fails on circular dependencies, and will return undefined
      // for the argument instead.
      if (argType === undefined) {
        throw new Error(`Injection error. Recursive dependency detected in constructor for type ${target.name} 
             with parameter at index ${index}`
        );
      }
      const overrideToken = getInjectionToken(target, index);
      const actualToken = overrideToken === undefined ? argType : overrideToken;
      let provider = this.providers.get(actualToken);
      return this.injectWithProvider(actualToken, provider);
    });
  }

  private getTokenName<T>(token: Token<T>) {
    return token instanceof InjectionToken
      ? token.injectionIdentifier
      : token.name;
  }
}

最初咱们来简略测试一下咱们后面开发的 IoC 容器,具体的测试代码如下所示:

// container.test.ts
import {Container} from "./container";
import {Injectable} from "./injectable";
import {Inject} from "./inject";
import {InjectionToken} from "./provider";

const API_URL = new InjectionToken("apiUrl");

@Injectable()
class HttpClient {}

@Injectable()
class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}}

const container = new Container();

container.addProvider({
  provide: API_URL,
  useValue: "https://www.semlinker.com/",
});

container.addProvider({provide: HttpClient, useClass: HttpClient});
container.addProvider({provide: HttpService, useClass: HttpService});

const httpService = container.inject(HttpService);
console.dir(httpService);

以上代码胜利运行后,控制台会输入以下后果:

HttpService {httpClient: HttpClient {},
  apiUrl: 'https://www.semlinker.com/' }

很显著该后果正是咱们所冀望的,这示意咱们 IoC 容器曾经能够失常工作了。当然在理论我的项目中,一个成熟的 IoC 容器还要思考很多货色,如果小伙伴想在我的项目中应用的话,阿宝哥倡议能够思考应用 InversifyJS 这个库。

若须要获取残缺 IoC 容器源码的话,可在 全栈修仙之路 公众号回复 ioc 关键字,即可获取。

七、参考资源

  • 维基百科 – 管制反转
  • Stackblitz – Car-Demo
  • Github – reflect-metadata
  • Metadata Proposal – ECMAScript
  • typescript-dependency-injection-in-200-loc

八、举荐浏览

  • 了不起的 TypeScript 入门教程
  • 了不起的 Deno 入门篇
  • 了不起的 Deno 实战教程
  • 你不晓得的 Blob
  • 你不晓得的 WeakMap
正文完
 0