依赖注入一直是Angular的一个最大特点和卖点。它允许在我们应用不同组件中注入依赖,而不需要知道这些依赖是如何创建的,或者它们需要的依赖关系是什么,可是,已证明了目前的Angular 1的依赖注入系统有一些问题,所以建立了下一代框架Angular 2来解决这些问题,在这篇文章中,我们将探索新的依赖注入系统。
在我们进入新的依赖注入系统之前,先让我们先了解什么是依赖注入,而Angular 1的问题又是什么问题。
依赖注入是一种模式
在ng-conf 2014中Vojta Jina有一个关于依赖注入的演讲,在这次演讲中,它讨论了在开发Angular 2中关于新的DI系统的想法和故事,他很清楚,我们可以把DI看成两件事:作为一种设计模式和作为一个框架。而前者用来解释DI模式,后者可以帮助我们的系统,维护和组装依赖关系。这篇文章中我想做同样的事来帮助我们理解这一概念。
我们首先考虑看看下面的代码。
class Car {
constructor() {
this.engine = new Engine();
this.tires = Tires.getInstance();
this.doors = app.get('doors');
}
}
在这里没什么特别的东西,我们有一个Car
类,我们在构造函数中建立了构建一辆汽车需要的对象,此代码有什么问题吗?你可以看到,构造函数不仅分配需要的依赖关系到内部属性,也知道如何创建这些对象。
例如engine
属性是使用Engine
构造函数创建的,Tires
似乎是一个单例的对象,doors
是从一个全局对象中获取的,全局对象类似于服务定位(service locator)。
这导致代码难以维护,甚至更难测试。想象一下你想测试这个类,在代码中你将如何替换依赖Engine为MockEngine? 当编写测试时,我们想要使用代码在不同的场景中测试,因此此每个场景需要它自己的配置。
如果我们想写测试代码,我们需要编写可重用的代码。只要所有依赖关系都满足,我们的代码应该在任何环境中工作。这给我们带来的结论是可测试的代码是可重用的代码,反之亦然。
那么如何才能更好的写出这样的代码,使它更容易测试?这是超级容易,你可能已经知道该怎么做。我们把代码改成这样:
class Car {
constructor(engine, tires, doors) {
this.engine = engine;
this.tires = tires;
this.doors = doors;
}
}
所有我们做的是,我们将期望所有需要依赖的对象从构造函数创建移动到构造函数的参数创建,在这个代码中没有具体的实现,我们从字面上把创建这些依赖关系的责任转移到一个更高的层次上。如果我们想创建一个Car对象,我们所要做的就是把所有需要的依赖关系传递给构造函数:
var car = new Car(
new Engine(),
new Tires(),
new Doors()
);
这太酷了,不是吗?依赖从我们的类脱离出来,在我们编写测试中,我们可以通过mock依赖实现测试
var car = new Car(
new MockEngine(),
new MockTires(),
new MockDoors()
);
这就是依赖注入。更具体一点,这个特定的模式也被称为构造函数注入。还有两个注入模式,setter
注入和interface
注入,但我们将不会在本文中介绍这些内容。
好酷,现在我们正在使用DI,但是什么时候来一个DI系统呢?如前所述,我们从字面上把依赖性创造的责任转移到一个更高的水平。这正是我们新的问题。谁来负责组装所有这些依赖关系?是我们。
function main() {
var engine = new Engine();
var tires = new Tires();
var doors = new Doors();
var car = new Car(engine, tires, doors);
car.drive();
}
我们必须保留一个main函数,这样做是相当危险的,尤其是当应用程序变得越来越大,如果我们能做点像这样的事情,那是不是更好?
function main() {
var injector = new Injector(...)
var car = injector.get(Car);
car.drive();
}
依赖注入作为一个框架
这是依赖注入作为框架的地方。众所周知,Angular 1有它自己的DI系统,允许我们注释服务和其他组件,让injector发现他们知道什么依赖需要实例化,例如,下面的代码演示了如何在Angular 1中注释我们的Car类:
class Car {
...
}
Car.$inject = ['Engine', 'Tires', 'Doors'];
然后,我们注册我们的Car作为一个服务,每当我们使用它时,我们得到一个单例,它不需要关心Car需要创建的依赖。
var app = angular.module('myApp', []);
app.service('Car', Car);
app.service('OtherService', function (Car) {
// instance of Car available
});
这一切都很酷,但事实证明,现有的DI有一些问题:
- 内部缓存 - 依赖是单例的,当我们请求一个服务时,它在每个应用的生命周期中只会创建一次,创建工厂来解决这个问题也是相当危险的。
- 命名空间碰撞 - 在应用程序中只能有一个“type”的标记。会有一个问题,如果我们有car服务,然而第3方扩展也引入了一个相同名字的服务。
- 内置框架 - Angular 1 Di被嵌入在整个框架中,我们无法使用它作为一个独立的系统用来解耦。
这些问题需要得到解决,以便使Angular的DI达到下一个水平。
Angular 2中得依赖注入
在看实际代码之前,让我们首先了解新的DI系统背后的概念。下面的图片说明了新的DI系统所需的组件:
在Angular 2中DI基本上是由三个东西组成的:
- Injector - injector对象是用来给我们创建依赖实例的API。
- Provider - provider类似食谱,告诉Injector如何创建一个依赖实例。绑定需要一个token,映射到一个工厂的函数,创建一个对象。
- Dependency - dependency是应该要创建的对象的type。
好吧,现在我们有一个概念,让我们看看翻译成代码是什么样子的。我们继续保持Car类的依赖关系。下面是我们如何能够利用Angular 2的DI拿到Car的一个实例:
import { Injector } from 'angular2/di';
var injector = Injector.resolveAndCreate([
Car,
Engine,
Tires,
Doors
]);
var car = injector.get(Car)
我们从Angular 2中导入Injector模块,这暴露一些静态api,Injector.resolveAndCreate()基本上是一个工厂函数用来创建一个injector,然后提供一个provider列表。我们稍后将探讨如何将class提供给provider,但现在我们将焦点关注在injector.get()方法上。在代码最后行告诉我们怎么样去获取一个Car实例,我们的inject怎么知道需要创建依赖关系来实例化一个Car,看看我们的Car类来解释下为什么...
import { Inject } from 'angular2/di';
class Car {
constructor(
@Inject(Engine) engine,
@Inject(Tires) tires,
@Inject(Doors) doors
) {
...
}
}
我们从框架中导入了Inject的模块,用它来装饰(decorator)我们的构造函数参数。如果你不知道decorator是什么,你可能会想读我们的文章 decorators 和 annotations两者的不同和使用ES5编写Angular2代码。
Inject decorator会在我们的类上附加些元信息,之后会被DI系统给读取,所以基本上我们在这里所做的是告诉DI第一个参数是Engine类型的实例,第二个参数是Tires类型,第3个参数是Doors类型。我们可以使用TypeScript重写这些代码,感觉有点更自然:
class Car {
constructor(engine: Engine, tires: Tires, doors: Doors) {
...
}
}
好的,我们的类声明它自己的依赖与DI可以读的实例信息,但Injector如何知道要如何创建这样一个对象?这就是provider的作用。记得resolveAndCreate()方法中,我们传递一个数组形式的类列表吗?
var injector = Injector.resolveAndCreate([
Car,
Engine,
Tires,
Doors
]);
同样,你可能会想知道这个类的列表应该是一个provider列表。,如果我把写得这些转换为更详细的语法,那么可能会变得有点更加清晰。
import {provide} from 'angular2/angular2';
var injector = Injector.resolveAndCreate([
provide(Car, {useClass: Car}),
provide(Engine, {useClass: Engine}),
provide(Tires, {useClass: Tires}),
provide(Doors {useClass: Doors})
]);
我们有一个provide函数,他会映射一个token到配置的object上,token可以是一种type或者字符串,如果你现在阅读那些providers,你很容易理解发生了什么,我们绑定Car类型到Car类上,Engine类型到Engine类上等。这是我们之前所谈到的食谱机制。
因此,我们不仅仅让injector知道哪种dependencies是被用到应用程序中的,我们还配置如何创建这些依赖关系的对象。
现在,下一个问题来了,我们要使用更长的写法而不是简写语法吗?我们如果可以写成Foo,那就没有理由写成provide(Foo, {useClass: Foo})
,对吗。这就是为什么我们开始首先使用简洁的语法。然而,较长的语法使我们能够做一些非常非常强大的事。看看下一个代码片段
provide(Engine, {useClass: OtherEngine})
对,我们可以绑定一个token到任何想要绑定的东西上,在这里绑定Engine token到OtherEngine类上,这意味着,当我们申请获取Engine类型时,我们会获取类OtherEngine的一个实例。
这是超级强大的,因为这不仅为了让我们防止名称冲突,我们也可以创建一个接口的类型并将它绑定到一个具体的实现。除此之外,我们可以在一个单一的地方不接触任何其他代码使用一个token换出它实际的依赖,
Angular 2中DI的几个其他绑定方法将在下一节中探索。
其他provider配置
有时候,我们不希望得到一个类的一个实例,通过更多的配置我们可以得到一个单一的值或者工厂方法,异步依赖关系也可以是我们的应用的一部分,这就是为什么Angular 2的DI的provider机制带有不止一个方法。让我们快速浏览一下它们。
####值
我们想要简单的绑定到值可以使用 {useValue: value}
provide(String, {useValue: 'Hello World'})
当我们要绑定到简单的配置值时,这就很方便了。
####别名
我们可以给一个token绑定一个别名token
provide(Engine, {useClass: Engine})
provide(V8, {useExisting: Engine})
####工厂
是的,我们最喜爱的工厂。
provide(Engine, {useFactory: () => {
return function () {
if (IS_V8) {
return new V8Engine();
} else {
return new V6Engine();
}
}
}})
当然,工厂可能有它自己的依赖关系。通过对工厂的依赖性很容易给工厂添加一个tokens列表:
provide(Engine, {
useFactory: (car, engine) => {
},
deps: [Car, Engine]
})
可选依赖
该@Optionaldecorator
让我们声明依赖可选。这迟早会有用,例如,我们的应用程序需要一个第三方库,如果不存在的话,可以fallback
class Car {
constructor(@Optional(jQuery) $) {
if (!$) {
// set up fallback
}
}
}
正如你所看到的,Angular 2 DI解决了Angular 1 DI的前3个问题。但还有一件事,我们还没有谈到呢。新的DI是否还是创建单例对象
短暂(Transient)的依赖和子Injector
如果我们想要一个短暂的依赖,每一次获取依赖都创建一个新的实例,我们有2个选择:
工厂会返回一个类的实例,这将不会是单例.
provide(Engine, {useFactory: () => {
return () => {
return new Engine();
}
}})
我们也可以使用 Injector.resolveAndCreateChild() 来创建一个child injector,一个child injector在绑定一个对象实例时,将不同于老的injector的实例。
var injector = Injector.resolveAndCreateChild([Engine]);
var childInjector = Injector.resolveAndCreateChild([Engine]);
injector.get(Engine) !== childInjector.get(Engine);
child injectors 也更加有趣。如果原来child injector上没有给定binding,他会查找绑定在parent injector上的token来进行绑定
图片显示了3个injector,其中有2个是child injector,每一个injector都有自己的配置,现在我要从第2个child injector获取Car类型的实例,Car对象会被该child injector创建,
然而,engine将会被第一个child injector创建,tires 和doors会被parent injector创建,这个有点像原型链。
我们甚至可以配置可见性的依赖关系,这将在另一篇文章中讲到Host and Visibility in Angular 2's Dependency Injection,
在Angular 2中如何使用?
现在我们已经学习了DI如果在Angular 2运作,你可能会想知道它是如何使用在框架本身,我们在建立Angular 2组件时,是否需要手动创建injector?
幸运的是,Angular花了大量的精力和时间去设计出一个很好的API使Angular组件中隐藏了所有的injector。
让我们来来下面这个简单的Angular 2组件
@Component({
selector: 'app',
template: '<h1>Hello !</h1>'
})
class App {
constructor() {
this.name = 'World';
}
}
bootstrap(App);
class NameService {
constructor() {
this.name = 'Pascal';
}
getName() {
return this.name;
}
}
现在为了在我们的应用中使用NameService,我们需要通过在应用injector中提供provider配置,但是我们该如何做?我们还没有创建一个injector。
bootstrap(),在引导时,会我们的应用程序创建一个的根injector,在其第2个参数创建一个provider列表,将直接传递到Injector,换句话说,我们需要:
bootstrap(App, [NameService]);
就是这样。现在我们在应用中使用@Inject decorator就可以使用NameService
class App {
constructor(@Inject(NameService) NameService) {
this.name = NameService.getName();
}
}
或者 使用typescript,我们可以使用参数类型来注入
class App {
constructor(NameService: NameService) {
this.name = NameService.getName();
}
}
真棒,一下子,我们再也没有任何Injector了,但是还有一件事是:如果我们想在特定的组件中有不同的依赖配置,我们需要怎么做?
比方说,我们有一个NameService实例,在应用程序中NameService实例类型将被广泛注入,但有一个特定组件应该得到另一个不同的NameService实例,可使用@component注解的providers属性,它允许我们添加providers到一个特定的组件(和它的子组件)。
@Component({
selector: 'app',
providers: [NameService]
})
@View({
template: '<h1>Hello !</h1>'
})
class App {
...
}
为了把事情说清楚:providers不配置将要注入的实例,而是为当前组件创建一个child injector并配置,如前所述,
我们也可以配置我们绑定的可见性,以更加具体的哪一个组件可以注入什么。例如。该viewProviders属性只允许依赖被当前组件使用。