Dependence Injection(依赖注入)
依赖注入是Angular中的最大的一个特性与卖点。它允许应用中不同的组件不需要显性地建立关联即可以相互调用。不同,Angular 1中的依赖注入仍然是存在着一些问题,这也是Angular 2完全重构了一套依赖注入系统的原因。Angular 1中的依赖注入系统主要存在的问题如下:
Internal Cache(内建缓存):依赖一般是被当做单例对待,任何一个服务在整个应用的生命周期中都应该只被创建一次。
Synchronous by default(默认异步):Angular 1中的服务创建不是异步创建的。
Namespace collision(命名空间冲突):在应用的生命周期中某个”type”的token是唯一的,如果我们自定义了一个名为Car的服务,而引入的第三方框架中也存在着同名的Car的服务,那么整个系统就会存在问题。
Built into the framework(框架内建):Angular 1的依赖注入是内建的,我们无法单独的进行使用。
Angular 2的依赖注入体系大概如下所示:
其中有几个关键的概念解释如下:
Injector(注入器):Injector就类似于Spring里面的ApplicationContext,提供了一系列的接口以供依赖实例的创建。
Binding(绑定):Binding的作用在于告诉Injector如何去为某个依赖创建实例。一个Binding需要映射到一个工厂类型的方法。
Dependence(依赖):一个Dependence是某个对象被创建的类型。
最简单的Angular 2中的依赖注入的方式如下:
import { Injector } from 'angular2/di';
var injector = Injector.resolveAndCreate([
Car,
Engine,
Tires,
Doors
]);
var car = injector.get(Car);
resolveAndCreate是一个静态的接口方法,根据输入的一系列的Binding来创建依赖的实例。而后,可以使用injector.get()方法来获取某个Type/Token对应的对象实例。而在使用这个依赖时,可以使用Angular 2内置的Inject:
import { Inject } from 'angular2/di';
class Car {
constructor(
@Inject(Engine) engine,
@Inject(Tires) tires,
@Inject(Doors) doors
) {
...
}
}
Inject装饰器会自动将元数据绑定到Car类的属性中,也可以改写为TypeScript的方式:
class Car {
constructor(engine: Engine, tires: Tires, doors: Doors) {
...
}
}
到这一步,某个类可以声明它自己的依赖,并且被DI解析所有该类的依赖项。但是Injector还需要从Binding中获取如何去创建这些对象实例的信息。上文中是直接在resolveAndCreate方法中传入了一系列的Type/Token,而如果使用完整的写法,应该使用toClass方法显性的将某个Type/Token映射到某个实例。
import { bind } from 'angular2/di';
var injector = Injector.resolveAndCreate([
bind(Car).toClass(Car),
bind(Engine).toClass(Engine),
bind(Tires).toClass(Tires),
bind(Doors).toClass(Doors)
]);
上述方法中的token可以是任意的类型或者一个字符串,这也就是所谓的Recipe机制的具体实现。在这样的一种Binding的帮助下,不仅仅Injector知道如何在应用过程中使用这些依赖,并且配置了如何创建这些依赖。
进一步考虑,如果在应用中已经确定了Foo类型,那又何必要写bind(Foo).toClass(Foo)这样的表达式,直接在程序中引入写死即可,而依赖注入的真正魅力在于:
bind(Engine).toClass(OtherEngine)
这样可以动态的为某个token绑定到依赖中,并且有效解决了命名空间冲突的问题。我们可以创建一个类似与接口的类型,然后将它指向到具体的类型中。就像Java中的Interface与Implementation。
Other binding instructions
有时候,我们并不一定需要将某个token绑定到某个类中,而是绑定到某个字符串值或者工厂方法中。
绑定到值
bind(String).toValue('Hello World')
绑定到别名
bind(Engine).toClass(Engine)
bind(V8).toAlias(Engine)
toAlias方法将某个token绑定到另一个token中。
绑定到某个工厂方法
bind(Engine).toFactory(() => {
if (IS_V8) {
return new V8Engine();
} else {
return new V6Engine();
}
})
当然,某个工厂方法可能也有其依赖项,只要简单地将依赖项指向到参数中并且添加到token列表中即可。
bind(Engine).toFactory((dep1, dep2) => {
if (IS_V8) {
return new V8Engine();
} else {
return new V6Engine();
}
}, [Token1, Token2])
Transient Dependencies and Child Injectors(短暂性传递与子注入器)
在上文中提及的依赖项往往都是单例化的,但是有时候我们需要的是一个短暂的,即非单例模式的依赖,总体来说有两种方式:
使用工厂模式
bind(Engine).toFactory(() => {
return new Engine();
})
子注入器
可以使用Injector.resolveAndCreateChild()这个方法迭代地创建子注入器,一个子注入器会继承父类注入器中声明的依赖项,但是如果子注入器中也是声明了某个依赖,那么它创建的实例与父注入器创建的同样的token的实例是不一致的:
var injector = Injector.resolveAndCreate([Engine]);
var childInjector = injector.resolveAndCreateChild([Engine]);
injector.get(Engine) !== childInjector.get(Engine);
Component Dependence
@Component({
selector: 'app'
})
@View({
template: '<h1>Hello !</h1>'
})
class App {
constructor() {
this.name = 'World';
}
}
bootstrap(App);
上述声明的Component是直接将name写死在了代码里,如果将获取名字的这部分提取出来作为一个单独的服务:
class NameService {
constructor() {
this.name = 'Pascal';
}
getName() {
return this.name;
}
}
如果需要使用NameService,那么在声明某个Component时候,就需要使用@Inject装饰器:
class App {
constructor(@Inject(NameService) NameService) {
this.name = NameService.getName();
}
}
如果是TypeScript,需要这么写:
class App {
constructor(NameService: NameService) {
this.name = NameService.getName();
}
}
而NameService的Injector以及Binding这一步,其实就是由bootstrap方法实现的:
bootstrap(App, [NameService]);
当然,也可以写的更加优雅:
@Component({
selector: 'app',
bindings: [NameService]
})
@View({
template: '<h1>Hello !</h1>'
})
class App {
...
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。