在阅读本文之前,希望你已经对Angular2的依赖注入系统有了一定的了解。
Angular2应用程序能够用任何语言编写,只要最终的编译结果是JavaScript文件就可以了。当我们用TypeScript来编写应用的时候,我们需要使用 decorators 来为我们的代码添加 metadata 。某些时候,我们可以忽略一些 decorators ,仅仅依靠类型注释就可以了。然而,当我们需要注入某些服务的时候,如果没有适当的 decorators 将会出现一些意想不到的错误。
本文将要大家共同探讨一下这些意想不到的错误是什么,为什么会出现,以及如何解决它。
##注入服务依赖 首先让我们先创建一个需要 DataService 依赖的 Angular2 组件。代码如下:
@Component({
selector: 'my-app',
template: `
<ul>
<li *ngFor="#item of items">{{item.name}}</li>
</ul>
`
})
class AppComponent {
items:Array<any>;
constructor(dataService: DataService) {
this.items = dataService.getItems();
}
}
DataService 是另一个简单的类(在 Angular2 中服务其实就是一个类),它提供了一个方法来返回它的一个属性值。
class DataService {
items:Array<any>;
constructor() {
this.items = [
{ name: 'Christoph Burgdorf' },
{ name: 'Pascal Precht' },
{ name: 'thoughtram' }
];
}
getItems() {
return this.items;
}
}
当然,为了能够使用该服务,我们必须为我们的 injector 添加一个 provider。当引导应用启动时,我们需要传入一个 provider 参数到 bootstrap()中。
bootstrap(AppComponent, [DataService]);
以上对了解过 Angular2 的依赖注入系统的读者来说并没有什么新奇的地方,如果你对此感到困惑不解,你可以先去官网的教程中了解一下什么是依赖注入系统(Dependency Injection)。
但是,在我们需要为我们的服务注入一个依赖的时候,问题出现了。比如说,我们需要在我们的 DataService 中使用 Http 服务来向远程服务器请求数据,这该怎么办呢?首先,我们还是需要为 injector 添加一个可以使用 Http服务的 provider。
import {HTTP_PROVIDERS} from 'angular2/http';
...
bootstrap(AppComponent, [HTTP_PROVIDERS, DataService]);
Angular的http模块封装在 HTTP_PROVIDERS 中,所有用来操作 Http 的 providers 都在其中。接下来,就是将 Http 的实例注入到我们的服务中去,让我们可以在服务中使用 Http服务。
import {Http} from 'angular2/http';
class DataService {
items:Array<any>;
constructor(http:Http) {
...
}
...
}
好的,完成了么?如果你以为就这么简单的话,那你就太天真了。现在我们运行一下我们的程序,你会发现,满满都是错误:
Cannot resolve all parameters for DataService(?). Make sure they all have valid type or annotations.
这段错误的意思是说由于Angular并不知道 Http
是什么类型所以不能解析 DataService 的 Http
依赖,并且也没有 provider 可以用来解析该依赖。机智的你很快会发现我们将Http
类型放在了构造函数中。
没错,就是在构造函数中。不幸的是,我们所做的工作还不够。但是好消息是我们确实已经将 DataService 注入到了我们的 AppComponent 组件中。那么为什么会出现刚刚的问题呢?让我们快速回顾一下依赖注入系统都需要的 metadata 的来源。在我的另一篇文章《decorators 与 annotations》中,我们知道 decorators 为我们的代码添加了 metadata 。我们可以看一下被转化成 JavaScript 文件之后的 AppComponent:
function AppComponent(myService) {
...
}
AppComponent = __decorate([
Component({...}),
View({...}),
__metadata('design:paramtypes', [DataService])
], AppComponent);
我们能很清楚地看到 Component、View 以及一些其它的 metadata (metadata)用来修饰 AppComponent。其中 paramtypes
是需要 Angular的依赖注入系统计算出来的,专门用来返回一个类型的实例。
我们再来看一下转换成 JavaScript 文件之后的 DataService:
DataService = (function () {
function DataService(http) {
...
}
return DataService;
})();
是不是发现什么不对的地方了?显然我们没有看到任何 metadata 的身影,为什么会这样呢?
当在 tsconfig.json 中 设置了emitDecoratorMetadata
的时候,TypeScript 就会产生 metadata 。但是它并不意味着要为我们代码中的每一个方法和类都产生 metadata 。TypeScript 只会为拥有 decorator 的属性、方法、类或方法和构造函数的参数产生 metadata 。如果不这样的话,会产生一大堆没有用的 metadata 代码,不仅影响了文件的体积,也会对我们的应用产生影响。
现在你应该明白了为什么只为 AppComponent 产生了 metadata ,而在 DataService 并没有 metadata 的原因了吧?我们的 AppComponent 确实有 decorators,否则它就不是一个组件了。
##强制产生 metadata
既然问题是因为没有产生 metadata ,那么我们该如何强制让 TypeScript 为我们在我们指定的地方产生 metadata 呢?我们可以使用 Angular 为依赖注入系统提供的 decorators。@Inject
就是用来为一个指定的类型寻找一个依赖。
我们来看一下修改之后的 DataService:
import {Inject} from 'angular2/core';
import {Http} from 'angular2/http';
class DataService {
items:Array<any>;
constructor(@Inject(Http) http:Http) {
...
}
...
}
如此一来,问题解决了!我们来看一看转化过后的 JavaScript 代码:
function DataService(http) {
}
DataService = __decorate([
__param(0, angular2_1.Inject(Http)),
__metadata('design:paramtypes', [Http])
], DataService);
虽然我们的代码中确实存在 metadata,但是我们不能将它整块的消除。我们可以稍微做下修改。我们之前说过,当 decorators 放到了代码的前面,就能产生 metadata。
我们试着把代码中的所有 decorators 提到类的声明的前面,或者放到构造函数的参数之前。换而言之,我们可以移除 @Inject
并用其它的东西替代它,只要把 decorator 放在类声明的前面。
当然,我们也不能随便找个 decorator 就放到一个类声明的前面,必须使用合适的 decorator。幸运的是,Angular 自带有一些我们可以使用的 decorator。一般用 @Injectable
来产生 metadata, 它在 TypeScript 中没有其他特殊的意思,但它却很适合我们的情况。
接下来的工作就变得简单了,我们只需要把它放在在 DataService 之前就可以了。
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
@Injectable()
class DataService {
items:Array<any>;
constructor(http:Http) {
...
}
...
}
这段代码中,@Injectable
会使 TypeScript 强制生成我们需要的 metadata,而 decorator 本身并没有任何特殊的含义。