
随着程序规模的增长,我们常常遇到应用模块需要相互通信的情况。当模块A需要模块B才能运行时,我们就说B是A的依赖。
获取依赖的最常见方式之一就是直接导入(import)一个文件。例如,在某个假想模块中,我们可以这么做:
// in A.ts
import {B} from 'B'; // a dependency!
B.foo(); // using B
通常,只要导入其他代码就足够了;但是在某些情况下,要用到更加精巧的方式提供依赖。
●如果我们想在测试时把B的实现替换为MockB,该怎么办呢?
●如果我们想在整个应用中共享B类的单一实例(比如单例模式),该怎么办呢?
●如果我们想在每次用到B类时都创建一个新实例(比如工厂模式),该怎么办呢?
依赖注入可以解决这些问题。
依赖注入(dependency injection,DI)是这样一个系统:它让程序中的某部分可以访问其他部分,而且我们可以配置它们的访问方式。
可以把注入器看作new操作符的替代品。
依赖注入这个术语既被用来描述一种设计模式(可用于很多种框架),也被用来指代Angular内置的DI实现库。
使用依赖注入技术的主要优点是客户代码不必知晓如何创建依赖,它们只需要与那些依赖交互就可以了。
假设我们有一个Product类。每个产品都有一个基准价格。我们要靠一个服务来计算该产品的含税价,它需要如下输入:
●产品的基准价格
下面是不使用依赖注入时的代码:
class Product {
constructor(basePrice:number){
this.service = new PriceService();
this.basePrice = basePrice;
}
price(state:string){
return this.service.calculate(this.basePrice, state);
}
}
想象一下,我们要为此Product类写一个测试。假设这个PriceService类要使用数据库查询来获得产品在指定州的税率。如果这样写测试的话:
let product;
beforeEach(()=> {
product = new Product(11);
});
describe('price',()=> {
it('is calculated based on the basePrice and the state',()=> {
expect(product.price('FL')).toBe(11.66);
});
})
尽管这个测试可以工作,但是暴露了一些缺陷。为了让这个测试成功运行,需要满足两个前提条件:
(1)数据库必须保持运行;
(2)佛罗里达州(代号FL)的税率必须始终像我们期望的一样。
根本原因在于:Product类和PriceService类(而它又依赖于数据库)之间突兀的强烈依赖会让我们的测试变得更脆弱。
如果稍微改写一下Product类呢?
class Product {
constructor(service:PriceService, basePrice:number){
this.service = service;
this.basePrice = basePrice;
}
price(state:string){
return this.service.calculate(this.basePrice, state);
}
}
现在,当要创建Product的实例时,客户方代码可以决定把PriceService的哪个具体实现传给这个新实例了。
这样,只要创建一个mock版本的PriceService类就可以大幅简化测试了:
class MockPriceService {
calculate(basePrice:number, state:string){
if(state === 'FL'){
return basePrice * 1.06;
}
return basePrice;
}
}
基于这个小改动,我们就可以微调测试,移除它对数据库的依赖:
let product;
beforeEach(()=> {
const service = new MockPriceService();
product = new Product(service, 11);
});
describe('price',()=> {
it('is calculated based on the basePrice and the state',()=> {
expect(product.price('FL')).toBe(11.66);
});
})
另一个好处是我们现在能更加确信自己正在不受外界影响地测试Product类。也就是说,我们能确保该类正在使用一个行为上可预测的依赖。
这种注入依赖的技术是基于一项被称为控制反转的设计原则。
控制反转(inversion of
control,IoC)原则的非正式称谓是“好莱坞法则”。它来自好莱坞的一句常用语“别打给我们,我们会打给你(don't call us, we'll call you)”。
多年以来,它在与全应用语境相关的部件(指组件、服务、管道等Angular代码块)中用得非常普遍,也常被用来解决依赖的创建和设置问题。这一点在例子中体现得很清楚:Product类不得不了解PriceService类。
问题在于,一旦部件变得过于关心它的依赖,部件本身就会变得脆弱而难以修改。如果我们修改了一个部件,这项修改就会向上扩散到所有依赖它的部件中。它会影响到程序中很多不同的区域,甚至可能超出程序的边界。换句话说,这些部件之间产生了紧耦合。
使用依赖注入,我们就可以得到一个更加松耦合的架构。这时,当修改单一部件时,对程序中其他区域的影响就小多了。同时,只要这些部件之间的接口没有变,我们甚至可以在不修改其他部件中实现代码的情况下集体更换它们。
Angular从AngularJS中继承来的一项伟大特性就是它们都使用这种控制反转模式。Angular使用自带的依赖注入机制来解析这些依赖。
在传统方式下,如果部件A需要依赖部件B,那就意味着A要在内部创建一个B的实例,也就是A依赖于B(如图8-1所示)。

图8-1 不用依赖注入框架时
Angular利用依赖注入机制改变了这一点。在这种机制下,如果需要在部件A中用到部件B,我们就应该期待B被传给A(如图8-2所示)。

图8-2 使用依赖注入框架时
在传统场景下,这带来了很多优点。一个优点就是:如果我们准备单独测试A,可以创建一个mock版本的B,并把它注入到A中。
在本书的前面,我们已经多次用过服务和依赖注入了,比如在第7章创建音乐应用时。为了与Spotify API交互,我们创建了SpotifyService。它被注入到了很多部件中,比如下面这个来自AlbumComponent的片段。
code/routes/music/app/ts/components/AlbumComponent.ts
export class AlbumComponent implements OnInit {
id:string;
album:Object;
constructor(private route:ActivatedRoute,
private spotify:SpotifyService, // <—— injected
private location:Location){
route.params.subscribe(params => { this.id = params['id']; });
}
现在,我们就来学习如何创建自己的服务以及能用哪些形式注入它们吧。
要注册一个依赖,我们就得找到一些东西作为那个依赖的标识。这个标识被称为依赖的令牌(token)。比如,如果我们想要注册某个API的URL,就可以用字符串API_URL作为令牌。同样,如果我们要注册一个类,就可以使用这个类本身作为它的令牌,就像我们即将看到的。
在Angular中,依赖注入包括如下三部分。
●提供者(也常被称为绑定)负责把一个令牌(可能是字符串也可能是类)映射到一个依赖的列表。它告诉Angular该如何根据指定的令牌创建对象。
●注入器负责持有一组绑定;当外界要求创建对象时,解析这些依赖并注入它们。
●依赖就是将被用于注入的对象。
我们可以借助图8-3来理解它们各自扮演的角色。

图8-3 依赖注入
与依赖注入打交道时,有很多不同的选项,我们来分别看看它们的用途。
最常见的情况是提供一个服务或值,它将在整个应用中保持一致。在我们的应用中,99%的场景可能都属于这种情况。
既然这就是我们要做的一切,那就在下一节示范怎样写一个基本的服务吧,因为它正是我们在开发大多数应用的大部分时间里所需要的。
说的够多了,开始编码!
就像前面提到过的,Angular会在幕后帮我们设置好依赖注入。不过,在我们和注解打交道并且把依赖注入集成到部件中之前,自己先尝试使用一下注入器。
先来创建一个直接返回字符串的示例服务。
code/dependency_injection/injector/app/ts/app.ts
/*
* The injectable service
*/
class MyService {
getValue():string {
return 'a value';
}
}
接下来,创建该应用的组件。
code/dependency_injection/injector/app/ts/app.ts
@Component({
selector:'di-sample-app',
template:`
<button(click)="invokeService()">Get Value</button>
`
})
class DiSampleApp {
myService:MyService;
constructor(){
let injector:any = ReflectiveInjector.resolveAndCreate([MyService]);
this.myService = injector.get(MyService);
console.log('Same instance?', this.myService === injector.get(MyService));
}
invokeService():void {
console.log('MyService returned', this.myService.getValue());
}
}
下面对这个过程进行分解。我们首先声明了DiSampleApp组件,它会渲染出一个按钮。当点击此按钮时就会调用invokeService方法。
仔细看该组件的构造函数就会发现,我们正在使用一个来自ReflectiveInjector的静态方法,名为resolveAndCreate。该方法负责创建一个新的注入器。我们传给它的参数是一个数组,其中是这个新注入器需要知道的可供注入物。在这个例子中,它知道MyService这个可注入物就够了。
ReflectiveInjector是Injector的一个具体实现,它使用反射(reflection)机制来找出正确的参数类型。虽然也有一些别的注入器,不过在大多数应用中,ReflectiveInjector应该是最常用的“常规”注入器了。
需要注意的一点是:它会注入该类的一个单例对象。
这可以从构造函数中的最后两行得到验证。首先要求刚创建的注入器给我们一个MyService类的实例,然后把它存入组件的myService字段。之后,在console.log函数中要求注入器再次给我们一个MyService的实例,并输出它与myService字段进行比较的结果:
console.log('Same instance?', this.myService === injector.get(MyService));
我们可以在控制台中确认这两个实例确实是指向同一个对象的引用:
Same instance? true
注意,由于使用了自己的注入器,我们并不需要在启动时把MyService加入NgModule的providers列表中。
code/dependency_injection/injector/app/ts/app.ts
@NgModule({
declarations:[ DiSampleApp ],
imports:[ BrowserModule ],
bootstrap:[ DiSampleApp ]
})
class DiSampleAppModule {}
platformBrowserDynamic().bootstrapModule(DiSampleAppModule);
不过,在正常情况下,还是得告诉NgModule要注入哪些提供者。
比如,我们想让该MyService单例对象在整个应用中都能被注入。
为了能够注入,必须把它们添加到NgModule的providers属性中。示例代码如下:
@NgModule({
declarations:[
MyAppComponent,
// other components ……
],
providers:[ MyService ] // <—— here
})
class MyAppModule {}
这样,MyAppComponent就能把MyService注入构造函数中了:
export class MyAppComponent {
constructor(private myService:MyService /* <—— injected */){
// do something with myService here
}
// ……
}
当我们把这个类本身放进providers中时:
providers:[ MyService ]
就是在告诉Angular:当MyService被注入时,我们希望提供MyService的一个单例实例。因为这种需求非常普遍,所以这个类实际上是一种缩写形式,其等价的完整配置方式是:
providers:[
{ provide:MyComponent, useClass:MyComponent }
]
除了创建类的实例之外,还有很多其他的方式可以进行注入,接下来就来逐个查看。
Angular的依赖注入体系有很多精巧之处,其中之一是我们有很多种方式来配置注入过程。比如可以:
●注入一个类的(单例)实例;
●调用任意函数,并注入该函数的返回结果;
●注入一个值;
●创建一个别名。
下面分别用例子进行解释。
8.6.1 使用类
注入类的单例实例大概是最常见的注入类型了。
配置方法如下:
{ provide:MyComponent, useClass:MyComponent }
需要注意的是:provide配置方法接收两个键(key)。第一个provide键是我们用作这个可注入对象标识的令牌,第二个useClass键用来指出注入什么以及如何注入。
在这里,我们把MyComponent类映射到了MyComponent令牌。在这个例子中,类名和令牌名是匹配的。这是最常见的情况,但是必须知道:令牌和被注入物并不一定同名。
如前所见,该例子中的注入器将会在幕后创建一个单例对象,并在每次注入它时返回同一个实例。
当然,首次注入时它尚未实例化,需要创建一个MyComponent实例。此时,依赖注入系统就会调用该类的构造函数。
如果服务的构造函数需要一些参数,会怎么样呢?假设我们有这样一个服务。
code/dependency_injection/misc/app/ts/app.ts
class ParamService {
constructor(private phrase:string){
console.log('ParamService is being created with phrase', phrase);
}
getValue():string {
return this.phrase;
}
}
注意,它的构造函数需要传入一个短语作为参数。如果我们使用标准注入机制,就会在浏览器中看到一个错误,如图8-4所示。

图8-4 注入错误
这是因为我们没有为注入器提供足够的信息来构造这个类。要解决这个问题,就得告诉注入器在创建该服务的实例时要使用哪个参数。
如果想在创建服务时传入一个参数,就要改用工厂了。
8.6.2 使用工厂
如果要使用工厂进行注入,就需要写一个返回任意对象的函数。
{
provide:MyComponent,
useFactory:()=> {
if(loggedIn){
return new MyLoggedComponent();
}
return new MyComponent();
}
}
注意,在这个例子中,我们注入时用的令牌是MyComponent,但是它会检查(作用域外面的)loggedIn变量。如果loggedIn为真,则注入器会返回一个MyLoggedComponent的实例;否则返回MyComponent的实例。
工厂还可以拥有自己的依赖:
{
provide:MyComponent,
useFactory:(user)=> {
if(user.loggedIn()){
return new MyLoggedComponent(user);
}
return new MyComponent();
},
deps:[ User ]
}
因此,如果要使用前面的ParamService,我们就得把它用useFactory包裹起来。
code/dependency_injection/misc/app/ts/app.ts
@NgModule({
declarations:[ DiSampleApp ],
imports:[ BrowserModule ],
bootstrap:[ DiSampleApp ],
providers:[
SimpleService,
{
provide:ParamService,
useFactory:():ParamService => new ParamService('YOLO')
}
]
})
class DiSampleAppAppModule {}
platformBrowserDynamic().bootstrapModule(DiSampleAppAppModule)
.catch((err:any)=> console.error(err));

我们可以把SimpleService直接放在providers列表中,这是因为SimpleService并不需要任何参数。它会被翻译成:
{ provide:SimpleService, useClass:SimpleService }
可以说,工厂是创建可注入对象的最强方式,因为我们可以在工厂函数中“为所欲为”。
8.6.3 使用值
当我们需要一个常量,而它可能会根据应用的其他部分甚至环境进行重定义时(比如测试环境或生产环境),这种方式非常有用。
{ provide:'API_URL', useValue:'http://my.api.com/v1' }
在8.9节中,我们会提供一个更完善的例子。
8.6.4 使用别名
我们还可以制造一个别名来引用以前注册过的令牌,比如:
{ provide:NewComponent, useClass:MyComponent }
当我们开发应用时,需要经过三步才能进行依赖注入:
(1)创建该服务的类;
(2)在准备接受注入的部件上声明该依赖;
(3)配置要注入的依赖(比如在我们的NgModule中通过Angular注册要注入的依赖)。
我们要做的第一件事是创建该服务的类,该类会暴露出我们想要用到的那些行为。它也被称为可注入对象,因为它就是我们的部件将通过依赖注入接收到的东西。
下面示范如何创建服务。
code/dependency_injection/simple/app/ts/services/ApiService.ts
export class ApiService {
get():void {
console.log('Getting resource……');
}
}
现在已经有了要注入的东西,接下来要声明当Angular创建部件时,我们希望接收哪些依赖。
我们以前直接使用Injector类,但在写部件时,我们通常会使用Angular提供的两种快捷方式。
第一种是在部件的构造函数中声明这些可注入对象。这也是最典型的用法。
要做到这一点,必须先导入该服务。
code/dependency_injection/simple/app/ts/app.ts
/*
* Services
*/
import { ApiService } from 'services/ApiService';
然后在构造函数中声明它。
code/dependency_injection/simple/app/ts/app.ts
class DiSampleApp {
constructor(private apiService:ApiService){
}
当我们在组件的构造函数中声明依赖时,Angular会通过反射机制来找出要注入的类。也就是说,Angular会发现我们正在构造函数中查找一个ApiService类型的对象,并检查依赖注入系统以找出合适的可注入对象。
有时我们需要给Angular更多的提示,来告诉它我们到底要注入什么。在这种情况下,我们要使用第二种方式,即@Inject注解。
class DiSampleApp {
private apiService:ApiService;
constructor(@Inject(ApiService)apiService){
this.apiService = apiService;
}
如果我们要用这种等价形式,可以打开app.long.ts文件,把它的内容复制到app.ts中。
使用依赖注入的最后一步是把部件想要的东西与可注入对象关联起来。换句话说,我们告诉Angular:当部件声明了它的依赖时,应该注入什么。
{ provide:ApiService, useClass:ApiService }
在这个例子中,我们使用令牌ApiService暴露出了ApiService类的单例对象。
最后,我们把这个ApiService添加到NgModule的providers属性中。
code/dependency_injection/simple/app/ts/app.ts
@NgModule({
declarations:[ DiSampleApp ],
imports:[ BrowserModule ],
bootstrap:[ DiSampleApp ],
providers:[ ApiService ] // <—— here
})
class DiSampleAppAppModule {}
platformBrowserDynamic().bootstrapModule(DiSampleAppAppModule)
.catch((err:any)=> console.error(err));
我们已经和注入器打过交道了,现在要更进一步,看看什么时候需要显式地使用它们。
情况之一是,当我们需要控制在什么时机创建依赖的单例对象时。
为了说明什么时候会出现这种情况,我们来构建另一个应用。除了使用我们以前创建过的ApiService外,它还会用到一个新的服务。
该服务将用来根据浏览器的窗口大小实例化另外两个服务。如果窗口宽度小于800像素,它就返回一个名叫SmallService的服务实例;否则返回LargeService的实例。
下面是SmallService的代码。
code/dependency_injection/complex/app/ts/services/SmallService.ts
export class SmallService {
run():void {
console.log('Small service……');
}
}
下面是LargeService的代码。
code/dependency_injection/complex/app/ts/services/LargeService.ts
export class LargeService {
run():void {
console.log('Large service……');
}
}
然后,我们开始写ViewPortService,它负责在两者之间作出选择。
code/dependency_injection/complex/app/ts/services/ViewPortService.ts
import {LargeService} from './LargeService';
import {SmallService} from './SmallService';
export class ViewPortService {
determineService():any {
let w:number = Math.max(document.documentElement.clientWidth,
window.innerWidth || 0);
if(w < 800){
return new SmallService();
}
return new LargeService();
}
}
现在,我们创建一个使用这些服务的应用。
code/dependency_injection/complex/app/ts/app.ts
class DiSampleApp {
constructor(private apiService:ApiService,
@Inject('ApiServiceAlias')private aliasService:ApiService,
@Inject('SizeService')private sizeService:any){
}
这里我们仍然用以前的方式获得一个ApiService的实例。不过这次我们通过别名'ApiServiceAlias'获得了同一个实例。最后,我们要获得一个'SizeService'的实例,但它还没有定义过。
为了理解每个服务都代表什么,我们来看看NgModule。
code/dependency_injection/complex/app/ts/app.ts
@NgModule({
declarations:[ DiSampleApp ],
imports:[ BrowserModule ],
bootstrap:[ DiSampleApp ],
providers:[
ApiService,
ViewPortService,
{ provide:'ApiServiceAlias', useExisting:ApiService },
{
provide:'SizeService',
useFactory:(viewport:any)=> {
return viewport.determineService();
},
deps:[ViewPortService]
}
]
})
class DiSampleAppAppModule {}
这段代码的意思是,我们首先希望该应用的注入器知道ApiService和ViewPortService这两个可注入对象。
接下来的声明表示是我们希望通过另一个令牌(字符串ApiServiceAlias)来使用既有服务ApiService。
然后,我们通过另一个字符串令牌SizeService定义了另一个可注入对象。该工厂通过把ViewPortService列在自己的deps数组中,表明自己需要接收该服务的一个实例。然后,它将调用该实例的determineService()方法,并根据浏览器的宽度返回一个SmallService或LargeService的实例。
当点击模板中的一个按钮时,我们会发起三次调用:一次是对ApiService,一次是对别名ApiServiceAlias,最后一次则是对SizeService。
code/dependency_injection/complex/app/ts/app.ts
invokeApi():void {
this.apiService.get();
this.aliasService.get();
this.sizeService.run();
}
现在,如果我们运行此应用并在小型浏览器窗口中点击Invoke API按钮,结果会如图8-5所示。

图8-5 小型浏览器窗口
我们会获得三条日志:一条来自ApiService,另一条来自别名服务,最后一条来自SmallService。
如果我们让浏览器窗口更大一点,刷新页面并再次点击按钮,结果会如图8-6所示。

图8-6 大型浏览器窗口
这样我们就会收到来自LargeService的日志。然而,如果把浏览器窗口调小一点,不刷新页面并再次点击按钮,收到的仍将是来自LargeService的日志,如图8-7所示。

图8-7 小型浏览器窗口:调整大小后
这是因为这个工厂函数只会被执行一次,也就是在应用启动时。
要解决这个问题,我们可以创建自己的注入器,并通过如下方式获得正确的服务实例。
code/dependency_injection/complex/app/ts/app.ts
useInjectors():void {
let injector:any = ReflectiveInjector.resolveAndCreate([
ViewPortService,
{
provide:'OtherSizeService',
useFactory:(viewport:any)=> {
return viewport.determineService();
},
deps:[ViewPortService]
}
]);
let sizeService:any = injector.get('OtherSizeService');
sizeService.run();
}
这里我们创建了一个注入器,它知道ViewPortService和另一个以字符串OtherSizeService为令牌的可注入对象。这个可注入对象与我们以前用过的SizeService使用同一个工厂。
最后,它使用我们创建的注入器来获得一个OtherSizeService的实例。
这时,如果我们在一个大型浏览器窗口中运行该应用并点击Use Injector按钮,就会收到一条来自LargeService的日志。然而,如果我们把窗口调小,即使不刷新页面,也能正常收到来自SmallService的日志。这是因为现在注入器是随需创建的,我们每次点击按钮时都会重新执行工厂函数。这真漂亮!
使用依赖注入的另一个理由是在运行期间改变被注入对象的硬编码值。当我们用一个API服务来向应用的后端API发起HTTP请求时,就会出现这种情况。在单元测试或集成测试的场景下,我们肯定不希望代码接触生产环境的数据库。这时,就可以写一个Mock的API服务,它可以天衣无缝地替换掉我们的具体实现。我们这就来详细解释一下。
比如,如果在开发环境下运行该应用,我们可能会接触与生产环境下不同的API服务器。
当我们发布一个开源或可复用的服务时,这就更加有用了。这种情况下,我们要允许调用者定义或改写API的URL。
我们来写一个简单的示例应用,它会根据自己是在生产模式还是开发模式运行来为API的URL注入不同的值。先从ApiService类开始。
code/dependency_injection/value/app/ts/services/ApiService.ts
import { Inject } from '@angular/core';
export const API_URL:string = 'API_URL';
export class ApiService {
constructor(@Inject(API_URL)private apiUrl:string){
}
get():void {
console.log(`Calling ${this.apiUrl}/endpoint……`);
}
}
我们先声明了一个常量,它会被用作API URL依赖的令牌。换句话说,Angular会根据字符串'API_URL'来存储要调用哪个URL的信息。这样,当我们使用@Inject(API_URL)时,就会把正确的值注入到apiUrl变量中。
注意,我们还同时导出了API_URL常量,这样客户方应用就可以从服务之外使用API_URL来注入正确的值。
现在,我们已经有了服务,接下来写一个应用组件,它将使用该服务,并根据所在的运行环境为URL提供不同的值。
code/dependency_injection/value/app/ts/app.ts
@Component({
selector:'di-value-app',
template:`
<button(click)="invokeApi()">Invoke API</button>
`
})
class DiValueApp {
constructor(private apiService:ApiService){
}
invokeApi():void {
this.apiService.get();
}
}
这是组件的源代码。在构造函数中,我们声明了一个ApiService类型的变量apiService。这时,Angular就能推断出我们需要一个ApiService型的依赖,并在运行时注入它。如果我们要让它更明确一点,那么可以这样写:
constructor(@Inject(ApiService)private apiService:ApiService){
}
该组件有一个Invoke API按钮。当点击此按钮时,我们调用ApiService的get()方法。此方法就会把我们正在使用的API_URL的值记录到控制台中。
下一步是使用提供者来配置本应用。
code/dependency_injection/value/app/ts/app.ts
const isProduction:boolean = false;
@NgModule({
declarations:[ DiValueApp ],
imports:[ BrowserModule ],
bootstrap:[ DiValueApp ],
providers:[
{ provide:ApiService, useClass:ApiService },
{
provide:API_URL,
useValue:isProduction ?
'https://production-api.sample.com':
'http://dev-api.sample.com'
}
]
})
class DiValueAppAppModule {}
platformBrowserDynamic().bootstrapModule(DiValueAppAppModule)
我们首先声明了一个名叫isProduction的常量,并把它设置为false。我们先假装做了点什么来检测自己是否是在生产模式下运行。这里可以先把它硬编码进去,也可以使用一些小技巧来实现它,比如使用webpack和一个.env文件。
最后,我们引导本应用,并设置两个提供者:一个用真正的实现类来提供ApiService,另一个则用来提供API_URL。如果在生产模式下运行,我们就使用某个值,否则用另一个。
要测试它,我们可以带上isProduction = true来运行本应用。然后点击该按钮,就会看到日志中记录了生产模式下的URL,如图8-8所示。

图8-8 生产环境
如果把它改成isProduction = false,就会看到开发模式下的URL,如图8-9所示。

图8-9 开发环境
NgModule是帮助编译器和依赖注入对依赖进行组织的方式。让我们看看为什么需要NgModule以及它们是如何工作的。
我们要剖析的是Angular中的编译器和依赖注入这两个角色。简而言之,Angular需要解决组件定义了哪些HTML标记(tag)以及这些依赖来自哪里这两个问题。
8.10.1 NgModule与JavaScript模块
你可能会疑惑:为什么我们需要一个新的模块系统呢?只用ES6/TypeScript的模块还不够吗?
这是因为虽然仍然要用import来把代码模块加载到JavaScript环境中,但NgModule体系却是Angular框架内部对依赖进行组织的一种方式。特别是围绕两个问题:编译出了哪些标记以及哪些依赖应该被注入其中。
8.10.2 编译器与组件
对于编译器来说,如果有一个带有自定义标记的Angular模板,你就得告诉编译器哪些标记是有效的(以及应该为它们附加上哪些功能)。
比如,假设你有这样一个组件:
@Component({
selector:'hello-world',
template:`<div>Hello world</div>`
})
class HelloWorld {
}
我们希望编译器知道下列HTML代码应该使用这个hello-world组件(这个hello-world可不是随便写的无效标签):
<div>
<hello-world></hello-world>
</div>
在AngularJS中,hello-world选择器应该已经在全局范围注册过了。在你的应用成长到发生命名冲突之前,这样做都很方便。比如,如果两个开源项目使用了相同的选择器,问题就很难解决。
如果你用过Angular RC.5之前的老版本,可能还记得那些版本需要你在@Component注解中指定一个directives选项。这种方式的优点是它不怎么需要“魔术”来移除表面的冲突。它的问题在于要为每个组件指定用到的所有指令,这样太繁琐了。
改用NgModule,我们可以在“模块”一级告诉Angular组件的依赖关系。我们会在稍后讲解更多内容。
8.10.3 依赖注入与提供者
回忆一下,依赖注入是一种让依赖在整个应用中可用的组织形式。它对简单的import代码形式进行了强化,让我们得以用一种标准化的方式来共享单例、创建工厂以及在测试期间改写依赖。
在Angular RC.5之前的版本中,我们不得不在bootstrap函数的providers参数中指定待注入的一切(提供者)。
回想下列术语:提供者提供(创建、实例化等)你想要的可注入对象。在Angular中,当你想要访问可注入对象时,就把一个依赖注入一个函数中。Angular中的依赖注入框架就会找到它,并把它提供给你。
现在,利用NgModule,每个提供者都被指定为模块的一部分。
现在你应该明白了为什么需要NgModule以及要怎样使用它了吧?这里是最简单的例子:
// app.ts
@NgModule({
imports:[ BrowserModule ],
declarations:[ HelloWorld ],
bootstrap:[ HelloWorld ]
})
class HelloWorldAppModule {}
platformBrowserDynamic().bootstrapModule(HelloWorldAppModule);
在这里,我们定义了一个HelloWorldAppModule类,随后将其作为我们应用程序的入口点。从RC5开始,不再使用组件来引导应用,而是改用bootstrapModule,就像这里的代码一样。
NgModule可以导入其他模块作为自己的依赖。我们要在浏览器中运行此应用,所以还要导入BrowserModule。
我们要在此应用中使用HelloWorld组件。记住这里的关键:每个组件都必须在某些NgModule中声明过。这里我们把HelloWorld放在了NgModule的declarations中。
我们说HelloWorld组件从属于HelloWorldAppModule;任何组件都只能从属于一个NgModule。
我们通常会把很多组件一起放进一个NgModule中,这很像Java中的package或C#中的namespace。
如果你想引导该模块(也就是把该模块作为应用的入口点),那么就得提供一个bootstrap属性,用它来指定一个作为该模块入口点的组件。
在这个例子中,你将会bootstrap这个HelloWorld组件,并把它作为根组件。不过,如果你创建的模块不需要用作应用程序入口点,那么bootstrap属性就是可选的。
8.10.4 组件可见性
要使用任何组件,当前的NgModule都必须先知道它。假设我们想在hello-world组件中使用user-greeting组件,就像这样:
<!—— hello-world template ——>
<div>
<user-greeting></user-greeting>
world
</div>
如果任何组件想要使用其他组件,它必须首先通过NgModule体系获得访问权。有两种方式能做到这一点:
(1)user-greeting组件位于同一个NgModule中(比如HelloWorldAppModule);
(2)HelloWorldAppModule导入(imports)了UserGreeting组件所在的模块。
假设我们要访问第二个路由。下面是UserGreetingModule中的UserGreeting组件的实现代码:
@Component({
selector:'user-greeting',
template:`<span>hello</span>`
})
class UserGreeting {
}
@NgModule({
declarations:[ UserGreeting ],
exports:[ UserGreeting ]
})
export class UserGreetingModule {}
注意,这里我们添加了一个新的属性exports。可以先把exports当作这个NgModule中公开组件的列表。这里隐含的意思是,我们可以轻松地制作一个私有组件,只要别把它列进exports中就行了。
如果你忘了把组件加到declarations和exports中(然后还要在另一个模块中通过imports引入本模块),那么组件将不会生效。为了让你的组件能在其他模块中通过imports的方式使用,你必须把组件同时放在这两个地方。
现在,只要把它导入到HelloWorldAppModule中,我们就可以在HelloWorld组件中使用它了,就像这样:
// updated HelloWorldAppModule
@NgModule({
declarations:[ HelloWorld ],
imports:[ BrowserModule, UserGreetingModule ], // <—— added
bootstrap:[ HelloWorld ],
})
class HelloWorldAppModule {}
8.10.5 指定提供者
只要把可注入对象的提供者添加到NgModule的providers属性中,就算完成指定了。
例如,假设我们有这样一个简单的服务:
export class ApiService {
get():void {
console.log('Getting resource……');
}
}
我们希望把它注入到组件中,就像这样:
class ApiDataComponent {
constructor(private apiService:ApiService){
}
getData():void {
this.apiService.get();
}
}
用NgModule可以很容易做到这一点:只要把ApiService传给该模块的providers属性就可以了:
@NgModule({
declarations:[ ApiDataComponent ],
providers:[ ApiService ] // <—— here
})
class ApiAppModule {}
这里直接传入ApiService实际上是一个缩写版本,使用provide的完整版本是这样的:
@NgModule({
declarations:[ ApiDataComponent ],
providers:[
provide(ApiService, { useClass:ApiService })
]
})
class ApiAppModule {}
我们是在告诉Angular,当ApiService被注入时,依赖注入体系要负责创建、维护该类的单例,并把它传进去。
要从其他模块中使用这些提供者,必须先导入(import)那个模块。
由于ApiDataComponent和ApiService都位于同一个NgModule中,ApiDataComponent可以直接注入ApiService。否则,就需要把包含ApiService的模块导入到ApiAppModule中。
可以看出,依赖注入和NgModule的协作为管理应用中的依赖提供了一种强大的方式。要了解更多知识,请参考下列资源:
●Victor Savkin对AngularJS中和Angular中依赖注入的对比