qrcode

马上订阅,开启修仙之旅

Angular 依赖注入简介

在介绍依赖注入的概念和作用前,我们先来看个例子。各位同学请睁大眼睛,我要开始 “闭门造车” 了。

一辆车内部构造很复杂,出于简单考虑,我们就只考虑三个部分:车身、车门和引擎。

现在我们来分别定义各个部分:

  1. 定义车身类
1
export default class Body { }
  1. 定义车门类
1
export default class Doors { }
  1. 定义车引擎类
1
2
3
4
5
export default class Engine {
start() {
console.log('开动鸟~~~');
}
}
  1. 定义汽车类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Engine from './engine';
import Doors from './doors';
import Body from './body';

export default class Car {
engine: Engine;
doors: Doors;
body: Body;

constructor() {
this.engine = new Engine();
this.body = new Body();
this.doors = new Doors();
}

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

一切已准备就绪,我们马上来造一辆车:

1
2
let car = new Car(); // 造辆新车
car.run(); // 开车上路咯

车已经可以成功上路,但却存在以下问题:

  • 问题一:在创建新车的时候,你没有选择,假设你想更换汽车引擎的话,按照目前的方案,是实现不了的。
  • 问题二:在汽车类内部,你需要在构造函数中手动去创建各个部件。

为了解决第一个问题,提供更灵活的方案,我们需要重构一下 Car 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class Car {
engine: Engine;
doors: Doors;
body: Body;

constructor(engine, body, doors) {
this.engine = engine;
this.body = body;
this.doors = doors;
}

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

重构完 Car 类,我们来重新造辆新车:

1
2
3
4
5
let engine = new NewEngine();
let body = new Body();
let doors = new Doors();
this.car = new Car(engine, body, doors);
this.car.run();

此时我们已经解决了上面提到的第一个问题,要解决第二个问题我们要先介绍一下依赖注入的概念。

依赖注入的概念

软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端) 。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务是将会变成客户端的状态的一部分。 传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。 —— 维基百科

控制反转 (inversion of control,IoC) 原则的非正式称谓是“好莱坞法则”。它来自好莱坞的一句常用语“别打给我们,我们会打给你 (don’t call us, we’ll call you)”。

一般情况下,如果服务 A 需要服务 B,那就意味着服务 A 要在内部创建服务 B 的实例,也就说服务 A 依赖于服务 B:

no-di

(图片来源 —— Angular 权威指南)

Angular 利用依赖注入机制改变了这一点,在该机制下,如果服务 A 中需要服务 B,即服务 A 依赖于服务 B,那么我们期望服务 B 能被自动注入到服务 A 中,如下图所示:

di

(图片来源 —— Angular 权威指南)

在 Angular 中,依赖注入包括以下三个部分:

  • 提供者负责把一个令牌(可能是字符串也可能是类)映射到一个依赖的列表。它告诉 Angular 该如何根据指定的令牌创建对象。
  • 注入器负责持有一组绑定;当外界要求创建对象时,解析这些依赖并注入它们。
  • 依赖就是将被用于注入的对象。

三者的关系图如下:

angular-di

(图片来源 —— Angular 权威指南)

接下来我们来看一下如何利用 Angular 重写上面的示例:

car.model.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export class Body {}
export class Doors {}
export class Engine {
start() {
console.log("?开动鸟~~~");
}
}

export class Car {
constructor(
private engine: Engine,
private doors: Doors,
private body: Body
) {}

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

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component({
selector: "app-root",
template: `
<div>
<h3>Angular DI</h3>
</div>
`
})
export class AppComponent {
carInjector: Injector;
constructor() {
this.carInjector = Injector.create([
{ provide: Body, useClass: Body, deps: [] },
{ provide: Doors, useClass: Doors, deps: [] },
{ provide: Engine, useClass: Engine, deps: [] },
{ provide: Car, useClass: Car, deps: [Engine, Doors, Body] }
]);
const newCar = this.createCar();
newCar.run();
}

createCar(): Car {
return this.carInjector.get(Car);
}
}

Provider

Provider 的作用

在 Angular 中我们通过 Provider 来描述与 Token 相关联的依赖对象的创建方式。在 Angular 中依赖对象的创建方式分为以下四种:

  • useClass
  • useValue
  • useExisting
  • useFactory

Provider 的分类

在 Angular 中 Provider 主要分为:

  • ClassProvider
  • ValueProvider
  • ExistingProvider
  • FactoryProvider

Provider 的使用

  1. ClassProvider
1
2
3
4
5
6
@Component({
selector: 'drink-viewer',
providers: [
{ provide: FoodService, useClass: FoodService }
],
})
  1. ValueProvider
1
2
3
4
5
6
7
8
9
@NgModule({
declarations: [
AppComponent,
],
providers: [
{ provide: 'api', useValue: '/api/pizzas' }
]
})
export class AppModule {}
  1. ExistingProvider
1
2
3
4
5
6
7
@Component({
selector: 'drink-viewer',
providers: [
FoodService,
{ provide: DrinkService, useExisting: FoodService }
]
})
  1. FactoryProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function SideFactory(http) {
return new FoodService(http, '/api/sides');
}

@Component({
selector: 'side-viewer',
providers: [
{
provide: FoodService,
useFactory: SideFactory,
deps: [Http]
}
],
})

在 ValueProvider 的示例中,我们使用字符串作为 token,在大多数情况下,是不会存在问题的。

1
{ provide: 'api', useValue: '/api/pizzas' }

但假设某一天我们引入了一个第三方库,该库内部也是使用 'api' 作为 token,这时候就会导致系统出现异常。

为了解决 token 冲突问题,Angular 引入了 InjectionToken 来避免出现 token 冲突。对于上面的示例,我们可以使用 InjectionToken 来创建一个唯一的 token:

1
export const API_TOKEN = new InjectionToken<string>('api');

使用的时候也很简单,只需要导入 API_TOKEN,然后更新 Provider 对象的 provide 属性,具体如下:

1
2
3
providers: [
{ provide: API_TOKEN, useValue: '/api/pizzas' }
]

最后我们来介绍一下 StaticProvider,Angular 为了提高应用的性能,引入了静态注入器和 StaticProvider。在引入 StaticProvider 之前,Angular 内部通过 Reflect API 自动解析依赖对象:

1
2
3
4
function _dependenciesFor(typeOrFunc: any): ReflectiveDependency[] {
const params = reflector.parameters(typeOrFunc);
//...
}

这个工作需要在运行时完成,而在 Angular 引入了静态注入器和 StaticProvider 之后,可以直接通过访问 Provider 对象的 provide 属性直接获取相应的依赖列表:

1
2
3
4
5
function computeDeps(provider: StaticProvider): DependencyRecord[] {
let deps: DependencyRecord[] = EMPTY;
const providerDeps: any[] =
(provider as ExistingProvider & StaticClassProvider & ConstructorProvider).deps;
}

这样在一定程度上,提高了应用程序的效率。最后感兴趣的同学,可以参考一下下图。

static-provider

(图片来源于网络)


欢迎小伙伴们订阅前端修仙之路,及时阅读 Angular、RxJS、TypeScript 和 Node.js 最新文章。

qrcode