前言
在使用Angular或者Nestjs时,你可能会遇到下面这种形式的代码:
import { Component } from '@angular/core';import { OtherService } from './other.service.ts';@Component({// 组件属性})export class AppComponent {constructor(public otherService: OtherService) {// 为什么这里的otherService会被自动传入}}
上述代码中使用了Component的装饰器,并在模块的providers中注入了需要使用的服务。这个时候,在AppComponent中otherService将会自动获取到OtherService实例。
你可能会比较好奇,Angular是如何实现这种神奇操作的呢?实现的过程简而言之,就是Angular在底层使用了IoC设计模式,并利用TypeScript强大的装饰器特性,完成了依赖注入。下面我会详细介绍IoC与DI,以及简单的DI实例。
理解了IoC与DI的原理,有助于我们更好的理解和使用> Angular及> Nestjs。
什么是 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依赖于ModuleA和ModuleB,产生了模块间的耦合。为了解决模块间的强耦合性,IoC的概念就产生了。
我们通过使用一个容器来管理我们的模块,这样模块之间的耦合性就降低了(下面这个例子只是模仿 IoC 的过程,Container 需要另外实现):
// container.jsimport { ModuleA } from './module-A';import { ModuleB } from './module-B';// Container是我们假设的一个模块容器export const container = new Container();container.bindModule(ModuleA);container.bindModule(ModuleB);// ModuleC.jsimport { container } from './container';class ModuleC {constructor() {this.a = container.getModule('ModuleA');this.b = container.getModule('ModuleB');}}
为了让大家更清楚 IoC 的过程,我举一个例子,方便大家理解。
当我要找工作的时候,我会去网上搜索想要的工作岗位,然后去投递简历,这个过程叫做控制正转,也就是说控制权在我的手上。而对于控制反转,找工作的过程就变成了,我把简历上传到拉钩这样的第三方平台(容器),第三方平台负责管理很多人的简历。此时HR(其他模块)如果想要招人,就会按照条件在第三方平台查询到我,然后再联系安排面试。
什么是 DI?
DI 英文全称为 Dependency Injection,即依赖注入。依赖注入是控制反转最常见的一种应用方式,即通过控制反转,在对象创建的时候,自动注入一些依赖对象。
如何使用 TypeScript 实现依赖注入?
在Nestjs或Angular中,我们需要通过装饰器@Injectable()让我们依赖注入到类实例中。而理解他们如何实现依赖注入,我们需要先对装饰器有所了解。下面我们简单的介绍一下什么是装饰器。
装饰器(Decorator)
TypeScript中的装饰器是基于ECMAScript标准的,而装饰器提案仍处于stage2,存在很多不稳定因素,而且API在未来可能会出现破坏性的更改,所以该特性在TS中仍是一个实验性特性,默认是不启用的(后面将会介绍如何配置开启)。
装饰器定义
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符(getter, setter),属性或参数上。装饰器采用@expression这种形式进行使用。
下面是使用装饰器的一个简单例子:
function demo(target) {// 在这里装饰target}@democlass DemoClass {}
装饰器工厂
如果我们需要定制装饰器,这个时候就需要一个工厂函数,返回一个装饰器,使用过程如下所示:
function decoratorFactory(value: string) {return function(target) {target.value = value;};}
装饰器组合
如果需要同时使用多个装饰器,可以使用@f @g x这种语法。
类装饰器
类装饰器是声明在类定义之前,可以用来监视、修改或替换类定义。类装饰器接收的参数就是类本身。
function addDemo(target) {// 此处的target就是DemoClasstarget.demo = 'demo';}@addDemoclass DemoClass {}
方法、属性、访问器的装饰器
装饰器运行时会被当做函数执行,方法和访问器接收下面三个参数:
- 对于静态属性来说是类的构造函数(Constructor),对于实例属性是类的原型对象(Prototype)。
- 属性(方法、属性、访问器)的名字。
- 属性的属性描述符(详情查看这个文档)。
特别地,对于属性装饰器只接收 1 和 2 这两个参数,没有第3个参数的原因是因为无法在定义原型对象时,描述实例上的属性。
通过下面这个例子,我们可以具体看一下这三个参数是什么,方便大家理解:
function decorator(target: any, key: string, descriptor: PropertyDescriptor) {}class Demo {// target -> Demo.prototype// key -> 'demo1'// descriptor -> undefined@decoratordemo1: string;// target -> Demo// key -> 'demo2'// descriptor -> PropertyDescriptor类型@decoratorstatic demo2: string = 'demo2';// target -> Demo.prototype// key -> 'demo3'// descriptor -> PropertyDescriptor类型@decoratorget demo3() {return 'demo3';}// target -> Demo.prototype// key -> 'method'// descriptor -> PropertyDescriptor类型method() {}}
参数装饰器
参数装饰器声明在一个参数声明之前。运行时当做函数被调用,这个函数接收下面三个参数:
- 对于静态属性来说是类的构造函数,对于实例属性是类的原型对象。
- 属性(函数)的名字。
- 参数在函数参数列表中的索引。
function parameterDecorator(target: Object,key: string | symbol,index: number) {}class Demo {// target -> Demo.prototype// key -> 'demo1'// index -> 0demo1(@parameterDecorator param1: string) {return param1;}}
TypeScript中的元数据(Metadata)
注意:元数据是 Angular 以及 Nestjs 依赖注入实现的基础,请务必看完本章节。 因为Decorators是实验性特性,所以如果想要支持装饰器功能,需要在tsconfig.json中添加以下配置。
使用元数据需要安装并引入{"compilerOptions": {"experimentalDecorators": true,"emitDecoratorMetadata": true}}
reflect-metadata这个库。这样在编译后的 js 文件中,就可以通过元数据获取类型信息。
你们应该会比较好奇,运行时JS是如何获取类型信息的呢?请紧张地继续往下看:// 引入reflect-metadataimport 'reflect-metadata';
引入了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] 即构造函数接收的参数@classDecoratorclass Demo {innerValue: string;constructor(val: string) {this.innerValue = val;}/** 元数据的值如下:* type -> String* paramtypes -> undefined* returntype -> undefined*/@propertyDecoratordemo1: string = 'demo1';/** 元数据的值如下:* type -> Function* paramtypes -> [String]* returntype -> String*/@propertyDecoratordemo2(str: string): string {return str;}}
上面的代码执行之后的返回如下所示:
[Function: Function] [ [Function: String] ] [Function: String][Function: String] undefined undefined[ [Function: String] ]
我列出了各种装饰器含有的元数据类型(即不是undefined的类型):
- 类装饰器:
design:paramtypes。 - 属性装饰器:
design:type。 - 参数装饰器、方法装饰器:
design:type、design:paramtypes、design:returntype。 访问器装饰器:
design:type、design:paramtypes。依赖注入(DI)
说了那么久,终于讲到了本篇文档最为关键的内容了🎉,本节的实现请确保元数据在你的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);}}// 依赖注入函数Factoryconst Factory = <T>(target: Constructor<T>): T => {// 获取target类的构造函数参数providersconst 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中,这样一个简单的依赖注入就完成了。 如果你发现本文中有错误或者不合适的地方,欢迎留言反馈。参考文献
- 装饰器·TypeScript
- Reflect Metadata
