Angular这个名字专指现在和未来的Angular版本,而AngularJS专指Angular的所有v1.x版本。
有很多大型AngularJS应用。 在决定迁移到Angular之前,首先要深入思考业务案例。 在这些案例中,最重要的部分之一是时间和需要付出的努力。 本章描述用于把AngularJS应用高效迁移到Angular平台的内置工具,每次讲一点点。
有些应用可能比其它的升级起来简单,还有一些方法能让把这项工作变得更简单。 即使在正式开始升级过程之前,我们可以准备AngularJS的程序,让它向Angular看齐。 这些准备步骤几乎都是关于如何让代码更加松耦合、更有可维护性,以及用现代开发工具提高速度的。 这意味着,这种准备工作不仅能让最终的升级变得更简单,而且还能提升AngularJS程序的质量。
成功升级的关键之一是增量式的实现它,通过在同一个应用中一起运行这两个框架,并且逐个把AngularJS的组件迁移到Angular中。
这意味着可以在不必打断其它业务的前提下,升级更大、更复杂的应用程序,因为这项工作可以多人协作完成,在一段时间内逐渐铺开。
Angular upgrade
模块的设计目标就是让你渐进、无缝的完成升级。
准备工作
Preparation
AngularJS应用程序的组织方式有很多种。当我们想把它们升级到Angular的时候, 有些做起来会比其它的更容易些。即使在我们开始升级之前,也有一些关键的技术和模式可以让我们将来升级时更轻松。
遵循Angular风格指南
Follow the Angular Style Guide
Angular风格指南收集了一些已证明能写出干净且可维护的AngularJS程序的模式与实践。 它包含了很多关于如何书写和组织Angular代码的有价值信息,同样重要的是,不应该采用的书写和组织Angular代码的方式。
Angular是一个基于AngularJS中最好的部分构思出来的版本。在这种意义上,它的目标和Angular风格指南是一样的: 保留AngularJS中好的部分,去掉坏的部分。当然,Angular还做了更多。 说这些的意思是:遵循这个风格指南可以让你写出更接近Angular程序的AngularJS程序。
有一些特别的规则可以让使用Angular的upgrade
模块进行增量升级变得更简单:
-
单一规则 规定每个文件应该只放一个组件。这不仅让组件更容易浏览和查找,而且还将允许我们逐个迁移它们的语言和框架。 在这个范例程序中,每个控制器、工厂和过滤器都在它自己的源文件中。
-
按特性分目录的结构和模块化规则在较高的抽象层定义了一些相似的原则:应用程序中的不同部分应该被分到不同的目录和Angular模块中。
如果应用程序能用这种方式把每个特性分到一个独立目录中,它也就能每次迁移一个特性。 对于那些还没有这么做的程序,强烈建议把应用这条规则作为准备步骤。而且这也不仅仅对升级有价值, 它还是一个通用的规则,可以让你的程序更“坚实”。
使用模块加载器
Using a Module Loader
当我们把应用代码分解成每个文件中放一个组件之后,我们通常会得到一个由大量相对较小的文件组成的项目结构。
这比组织成少量大文件要整洁得多,但如果你不得不通过<script>
标签在HTML页面中加载所有这些文件,那就不好玩了。
尤其是当你不得不按正确的顺序维护这些标签时更是如此。
这就是为什么开始使用模块加载器是一个好主意了。
使用模块加载器,比如SystemJS、
Webpack或Browserify,
可以让我们在程序中使用TypeScript或ES2015语言内置的模块系统。
我们可以使用import
和export
特性来明确指定哪些代码应该以及将会被在程序的不同部分之间共享。
对于ES5程序来说,我们可以改用CommonJS风格的require
和module.exports
特性代替。
无是论哪种情况,模块加载器都会按正确的顺序加载程序中用到的所有代码。
当我们的应用程序投入生产环境时,模块加载器也会让把所有这些文件打成完整的产品包变得更容易。
迁移到TypeScript
Migrating to TypeScript
Angular升级计划的一部分是引入TypeScript,即使在开始升级之前,引入TypeScript编译器也是有意义的。 这意味着等真正升级的时候需要学习和思考的东西更少。 它还意味着我们可以在AngularJS代码中开始使用TypeScript的特性。
因为TypeScript是ECMAScript 2015的一个超集,而ES2015又是ECMAScript 5的一个超集。
这意味着除了安装一个TypeScript编译器,并把文件名都从*.js
改成*.ts
之外,其实什么都不用做。
当然,如果仅仅这样做也没什么大用,也没什么令人兴奋之处。
下面这些额外步骤可以让我们精神抖擞起来:
-
对那些使用了模块加载器的程序,TypeScript的导入和导出(这实际上是ECMAScript 2015导入和导出)可以把代码组织到模块中。
-
类型注解可以逐步添加到已存在的函数和变量上,以固定它们的类型,并获得其优点:比如编译期错误检查、更好的支持自动完成,以及内联式文档等。
-
那些ES2015中新增的特性,比如箭头函数、
let
、const
、默认函数参数、解构赋值等也能逐渐添加进来,让代码更有表现力。 -
服务和控制器可以转成类。这样我们就能一步步接近Angular的服务和组件类了,这样等到我们开始升级时,也会更简单。
使用组件型指令
Using Component Directives
在Angular中,组件是用来构建用户界面的主要元素。我们把UI中的不同部分定义成组件,然后通过在模板中使用这些组件最终合成为UI。
我们在AngularJS中也能这么做。那就是一种定义了自己的模板、控制器和输入/输出绑定的指令 —— 跟Angular中对组件的定义是一样的。
要迁移到Angular,通过组件型指令构建的应用程序会比直接用ng-controller
、ng-include
和作用域继承等底层特性构建的要容易得多。
要与Angular兼容,AngularJS的组件型指令应该配置下列属性:
-
restrict: 'E'
。组件通常会以元素的方式使用。 -
scope: {}
- 一个独立作用域。在Angular中,组件永远是从它们的环境中被隔离出来的,在AngularJS中,我们也应该这么做。 -
bindToController: {}
。组件的输入和输出应该绑定到控制器,而不是$scope
。 -
controller
和controllerAs
。组件有它们自己的控制器。 -
template
或templateUrl
。组件有它们自己的模板。
组件型指令还可能使用下列属性:
-
transclude: true
:如果组件需要从其它地方透传内容,就设置它。 -
require
:如果组件需要和父组件的控制器通讯,就设置它。
组件型指令不能使用下列属性:
-
compile
。它在Angular中将不再被支持。 -
replace: true
。Angular永远不会用组件模板替换一个组件元素。这个特性在AngularJS中也同样不建议使用了。 -
priority
和terminal
。虽然AngularJS的组件可能使用这些,但它们在Angular中已经没用了,并且最好不要再写依赖它们的代码。
AngularJS中一个完全向Angular架构对齐过的组件型指令是这样的:
export function heroDetailDirective() {
return {
restrict: 'E',
scope: {},
bindToController: {
hero: '=',
deleted: '&'
},
template: `
<h2>{{ctrl.hero.name}} details!</h2>
<div><label>id: </label>{{ctrl.hero.id}}</div>
<button ng-click="ctrl.onDelete()">Delete</button>
`,
controller: function() {
this.onDelete = () => {
this.deleted({hero: this.hero});
};
},
controllerAs: 'ctrl'
};
}
AngularJS.5引入了组件API,它让像这样定义指令变得更简单了。 为组件型指令使用这个API是一个好主意,因为:
-
它需要更少的样板代码。
-
它强制使用组件的最佳实践,比如
controllerAs
。 -
对于指令中像
scope
和restrict
这样的属性,它有良好的默认值。
如果使用这个组件API进行快捷定义,那么上面看到的组件型指令就变成了这样:
export const heroDetail = {
bindings: {
hero: '<',
deleted: '&'
},
template: `
<h2>{{$ctrl.hero.name}} details!</h2>
<div><label>id: </label>{{$ctrl.hero.id}}</div>
<button ng-click="$ctrl.onDelete()">Delete</button>
`,
controller: function() {
this.onDelete = () => {
this.deleted(this.hero);
};
}
};
import { Directive, ElementRef, Injector, Input, Output, EventEmitter } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';
import { Hero } from '../hero';
@Directive({
selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent {
@Input() hero: Hero;
@Output() deleted: EventEmitter<Hero>;
constructor(elementRef: ElementRef, injector: Injector) {
super('heroDetail', elementRef, injector);
}
}
控制器的生命周期钩子$onInit()
、$onDestroy()
和$onChanges()
是AngularJS.5引入的另一些便利特性。
它们都很接近于Angular中的等价物,所以,围绕它们组织组件生命周期的逻辑会更容易升级。
使用升级适配器进行升级
Upgrading with The Upgrade Module
不管要升级什么,Angular中的upgrade
模块都会是一个非常有用的工具 —— 除非是小到没功能的应用。
借助它,我们可以在同一个应用程序中混用并匹配AngularJS和2的组件,并让它们实现无缝的互操作。
这意味着我们不用必须一次性做完所有升级工作,因为在整个演进过程中,这两个框架可以很自然的和睦相处。
升级模块如何工作
How The Upgrade Module Works
upgrade
模块提供的主要工具叫做UpgradeModule
。这是一个服务,它可以引导并管理同时支持Angular和AngularJS的混合式应用程序。
当使用UpgradeModule
时,我们实际做的是同时运行两个版本的Angular。所有Angular的代码运行在Angular框架中,
而AngularJS的代码运行在AngularJS框架中。所有这些都是真实的、全功能的框架版本。
没有进行任何仿真,所以我们可以期待同时存在这两个框架的所有特性和天生的行为。
所有这些事情的背后,本质上是一个框架中管理的组件和服务能和来自另一个中的进行互操作。 这发生在三个主要的领域:依赖注入、DOM和变更检测。
依赖注入
Dependency Injection
无论是在AngularJS中还是在Angular中,依赖注入都处于前沿和中心的位置,但在两个框架的工作原理上,却存在着一些关键的不同之处。
AngularJS | Angular |
---|---|
依赖注入的令牌(Token)永远是字符串(译注:指服务名称)。 |
令牌可能有不同的类型。 通常是类,也可能是字符串。 |
只有一个注入器。即使在多模块的应用程序中,每样东西也都被装入一个巨大的命名空间中。 |
有一组树状多层注入器,有一个根注入器,每个组件也另外有一个注入器。 |
就算有这么多不同点,也并不妨碍我们在依赖注入时进行互操作。UpgradeModule
解决了这些差异,并让它们无缝的对接:
-
通过升级它们,我们就能让那些在AngularJS中能被注入的服务在Angular的代码中可用。 在框架之间共享的是服务的同一个单例对象。在Angular中,这些外来服务总是被放在根注入器中,并可用于所有组件。 它们总是具有字符串令牌 —— 跟它们在AngularJS中的令牌相同。
-
通过降级它们,我们也能让那些在Angular中能被注入的服务在AngularJS的代码中可用。 只有那些来自Angular根注入器的服务才能被降级。同样的,在框架之间共享的是同一个单例对象。 当我们注册一个要降级的服务时,要明确指定一个打算在AngularJS中使用的字符串令牌。

组件与DOM
Components and the DOM
在混合式应用中,我们能同时发现那些来自AngularJS和Angular中组件和指令的DOM。
这些组件通过它们各自框架中的输入和输出绑定来互相通讯,它们由UpgradeModule
桥接在一起。
它们也能通过共享被注入的依赖彼此通讯,就像前面所说的那样。
要弄明白在一个混合式应用的DOM中发生了什么,有两点很关键:
-
DOM中的每个元素都只能被两个框架之一拥有。另一个框架会忽略它。 如果一个元素被AngularJS拥有,Angular就会当它不存在。反之亦然。
-
应用的根节点总是来自AngularJS中的模板。
所以,混合式应用总是像AngularJS程序那样启动,处理根模板的也是AngularJS. 然后,当这个应用的模板中使用到了Angular的组件时,Angular才开始参与。 这个组件的视图由Angular进行管理,而且它还可以使用一系列的Angular组件和指令。
更进一步说,我们可以按照需要,任意穿插使用这两个框架。 使用下面的两种方式之一,我们可以自由穿梭于这两个框架的边界:
-
通过使用来自另一个框架的组件:AngularJS的模板中用到了Angular的组件,或者Angular的模板中使用了AngularJS的组件。
-
通过透传(transclude)或投影(project)来自另一个框架的内容。
UpgradeModule
牵线搭桥,把AngularJS的透传概念和Angular的内容投影概念关联起来。

当我们使用一个属于另一个框架的组件时,就会发生一个跨框架边界的切换。不过,这种切换只发生在该组件元素的子节点上。 考虑一个场景,我们从AngularJS中使用一个Angular组件,就像这样:
<a-component></a-component>
此时,<a-component>
这个DOM元素仍然由AngularJS管理,因为它是在AngularJS的模板中定义的。
这也意味着你可以往它上面添加别的AngularJS指令,却不能添加Angular的指令。
只有在<a-component>
组件的模板中才是Angular的天下。同样的规则也适用于在Angular中使用AngularJS组件型指令的情况。
变更检测
Change Detection
AngularJS中的变更检测全是关于scope.$apply()
的。在每个事件发生之后,scope.$apply()
就会被调用。
这或者由框架自动调用,或者在某些情况下由我们自己的代码手动调用。它是发生变更检测以及更新数据绑定的时间点。
在Angular中,事情有点不一样。虽然变更检测仍然会在每一个事件之后发生,却不再需要每次调用scope.$apply()
了。
这是因为所有Angular代码都运行在一个叫做Angular zone的地方。
Angular总是知道什么时候代码执行完了,也就知道了它什么时候应该触发变更检测。代码本身并不需要调用scope.$apply()
或其它类似的东西。
在这种混合式应用的案例中,UpgradeModule
在AngularJS的方法和Angular的方法之间建立了桥梁。发生了什么呢?
-
应用中发生的每件事都运行在Angular的zone里。 无论事件发生在AngularJS还是Angular的代码中,都是如此。
-
UpgradeModule
将在每一次离开Angular zone时调用AngularJS的$rootScope.$apply()
。这样也就同样会在每个事件之后触发AngularJS的变更检测。

在实践中,这意味着我们不用在自己的代码中调用$apply()
,而不用管这段代码是在AngularJS还是Angular中。
UpgradeModule
都替我们做了。我们仍然可以调用$apply()
,也就是说我们不必从现有代码中移除此调用。
但是在混合式应用中,那些调用没有任何效果。
当我们降级一个Angular组件,然后把它用于AngularJS中时,组件的输入属性就会被AngularJS的变更检测体系监视起来。 当那些输入属性发生变化时,组件中相应的属性就会被设置。我们也能通过实现OnChanges 接口来挂钩到这些更改,就像它未被降级时一样。
相应的,当我们把AngularJS的组件升级给Angular使用时,在这个组件型指令的scope
(或bindToController
)中定义的所有绑定,
都将被挂钩到Angular的变更检测体系中。它们将和标准的Angular输入属性被同等对待,并当它们发生变化时设置回scope(或控制器)上。
通过Angular的NgModule来使用UpgradeModule
Using UpgradeModule with Angular NgModules
AngularJS还是Angular都有自己的模块概念,来帮你我们把应用组织成一些紧密相关的功能块。
它们在架构和实现的细节上有着显著的不同。
在AngularJS中,我们会把AngularJS的资源添加到angular.module
属性上。
在Angular中,我们会创建一个或多个带有NgModule
装饰器的类,这些装饰器用来在元数据中描述Angular资源。差异主要来自这里。
在混合式应用中,我们同时运行了两个版本的Angular。
这意味着我们至少需要AngularJS和Angular各提供一个模块。
当我们使用AngularJS的模块进行引导时,就得把Anuglar 2的模块传给UpgradeModule
。我们来看看怎么做。
要了解Angular模块的更多信息,请参阅Angular模块页。
引导AngularJS+2的混合式应用程序
Bootstrapping hybrid applications
使用UpgradeModule
升级应用的第一步总是把它引导成一个同时支持AngularJS和Angular的混合式应用。
纯粹的AngularJS应用可以用两种方式引导:在HTML页面中的某处使用ng-app
指令,或者从JavaScript中调用
angular.bootstrap。
在Angular中,只有第二种方法是可行的,因为它没有ng-app
指令。在混合式应用中也同样只能用第二种方法。
所以,在将AngularJS应用切换到混合模式之前,把它改为用JavaScript引导的方式是一个不错的起点。
比如说我们有个由ng-app
驱动的引导过程,就像这个:
<!DOCTYPE HTML>
<html>
<head>
<base href="/">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js"></script>
<script src="app/ajs-ng-app/app.module.js"></script>
</head>
<body ng-app="heroApp" ng-strict-di>
<div id="message" ng-controller="MainCtrl as mainCtrl">
{{ mainCtrl.message }}
</div>
</body>
</html>
我们可以从HTML中移除ng-app
和ng-strict-di
指令,改为从JavaScript中调用angular.bootstrap
,它能达到同样效果:
angular.bootstrap(document.body, ['heroApp'], {strictDi: true});
现在,把Angular引入项目中。根据搭建本地开发环境中的指导,你可以有选择的从“快速起步”的Github仓库中拷贝素材进来。
接下来,创建一个app.module.ts
文件,并添加下列NgModule
类:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
]
})
export class AppModule {
ngDoBootstrap() {}
}
这个最小化的NgModule
导入了BrowserModule
,该模块是每个基于浏览器的Angular应用都必须具备的。
它还从@angular/upgrade/static
导入了UpgradeModule
,并添加了一个ngDoBootstrap
空的覆盖方法,防止
Angular启动它自身。
现在我们使用platformBrowserDynamic
的bootstrapModule
方法启动AppModule
。
接着我们使用依赖注入来获取AppModule
中的一个UpgradeModule
实例,
并使用它来启动我们的AngularJS应用。
upgrade.bootstrap
方法接受与angular.bootstrap相同的参数。
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => {
const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.body, ['heroApp'], {strictDi: true});
});
我们还需要通过npm install @angular/upgrade --save
命令安装@angular/upgrade
包,并且为@angular/upgrade/static
包添加一个映射关系:
systemjs.config.js (map)
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
恭喜!我们就要开始运行AngularJS+2的混合式应用程序了!所有现存的AngularJS代码会像以前一样正常工作,但是我们现在也同样可以运行Angular代码了。
在AngularJS的代码中使用Angular的组件
Using Angular Components from AngularJS Code

一旦我们开始运行混合式应用,我们就可以开始逐渐升级代码了。做这件事的一种更常见的模式就是在AngularJS的上下文中使用Angular的组件。 该组件可能是全新的,也可能是把原本AngularJS的组件用Angular重写而成的。
假设我们有一个简单的用来显示英雄信息的Angular组件:
hero-detail.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'hero-detail',
template: `
<h2>Windstorm details!</h2>
<div><label>id: </label>1</div>
`
})
export class HeroDetailComponent { }
如果我们想在AngularJS中使用这个组件,我们就得用downgradeComponent()
方法把它降级。
如果我们这么做,就会得到一个AngularJS的指令,我们可以把它注册到AngularJS的模块中:
import { HeroDetailComponent } from './hero-detail.component';
/* . . . */
import { downgradeComponent } from '@angular/upgrade/static';
angular.module('heroApp', [])
.directive(
'heroDetail',
downgradeComponent({component: HeroDetailComponent}) as angular.IDirectiveFactory
);
由于HeroDetailComponent
是一个Angular组件,所以我们必须同时把它加入AppModule
的declarations
字段中。
并且由于这个组件在AngularJS模块中使用,也是我们Angular应用的一个入口点,我们还需要
将它加入到Angular模块的entryComponents
列表中。
import { HeroDetailComponent } from './hero-detail.component';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
],
declarations: [
HeroDetailComponent
],
entryComponents: [
HeroDetailComponent
]
})
export class AppModule {
ngDoBootstrap() {}
}
所有Angular组件、指令和管道都必须声明在NgModule中。
这里我们得到的是一个叫做heroDetail
的AngularJS指令,我们可以像用其它指令一样把它用在AngularJS模板中。
<hero-detail></hero-detail>
注意,它在AngularJS中是一个名叫heroDetail
的元素型指令(restrict: 'E'
)。
AngularJS的元素型指令是基于它的名字匹配的。
Angular组件中的selector
元数据,在降级后的版本中会被忽略。
当然,大多数组件都不像这个这么简单。它们中很多都有输入属性和输出属性,来把它们连接到外部世界。 Angular的英雄详情组件带有像这样的输入属性与输出属性:
hero-detail.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'hero-detail',
template: `
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<button (click)="onDelete()">Delete</button>
`
})
export class HeroDetailComponent {
@Input() hero: Hero;
@Output() deleted = new EventEmitter<Hero>();
onDelete() {
this.deleted.emit(this.hero);
}
}
这些输入属性和输出属性的值来自于AngularJS的模板,而downgradeComponent()
方法负责桥接它们:
import { HeroDetailComponent } from './hero-detail.component';
/* . . . */
angular.module('heroApp', [])
.controller('MainController', MainController)
.directive('heroDetail', downgradeComponent({
component: HeroDetailComponent,
inputs: ['hero'],
outputs: ['deleted']
}) as angular.IDirectiveFactory);
<div ng-controller="MainController as mainCtrl">
<hero-detail [hero]="mainCtrl.hero"
(deleted)="mainCtrl.onDelete($event)">
</hero-detail>
</div>
注意,虽然我们正在AngularJS的模板中,但却在使用Angular的属性(Attribute)语法来绑定到输入属性与输出属性。 这是降级的组件本身要求的。而表达式本身仍然是标准的AngularJS表达式。
为降级过的组件使用Angular的属性(Attribute)语法规则时有一个值得注意的例外。 它适用于由多个单词组成的输入或输出属性。在Angular中,我们要使用小驼峰命名法绑定这些属性:
[myHero]="hero"
但是从AngularJS的模板中使用它们时,我们得使用中线命名法:
[my-hero]="hero"
$event
变量能被用在输出属性里,以访问这个事件所发出的对象。这个案例中它是Hero
对象,因为this.deleted.emit()
函数曾把它传了出来。
由于这是一个AngularJS模板,虽然它已经有了Angular中绑定的属性(Attribute),我们仍可以在这个元素上使用其它AngularJS指令。
例如,我们可以用ng-repeat
简单的制作该组件的多份拷贝:
<div ng-controller="MainController as mainCtrl">
<hero-detail [hero]="hero"
(deleted)="mainCtrl.onDelete($event)"
ng-repeat="hero in mainCtrl.heroes">
</hero-detail>
</div>
从Angular代码中使用AngularJS组件型指令
Using AngularJS Component Directives from Angular Code

现在,我们已经能在Angular中写一个组件,并把它用于AngularJS代码中了。
当我们从低级组件开始移植,并往上走时,这非常有用。但在另外一些情况下,从相反的方向进行移植会更加方便:
从高级组件开始,然后往下走。这也同样能用UpgradeModule
完成。
我们可以升级AngularJS组件型指令,然后从Angular中用它们。
不是所有种类的AngularJS指令都能升级。该指令必须是一个严格的组件型指令,具有上面的准备指南中描述的那些特征。 确保兼容性的最安全的方式是AngularJS.5中引入的组件API。
可升级组件的简单例子是只有一个模板和一个控制器的指令:
hero-detail.component.ts
export const heroDetail = {
template: `
<h2>Windstorm details!</h2>
<div><label>id: </label>1</div>
`,
controller: function() {
}
};
我们可以使用UpgradeComponent
方法来把这个组件升级到Angular。
具体方法是创建一个Angular指令,继承UpgradeComponent
,在其构造函数中进行super
调用,
这样我们就得到一个完全升级的AngularJS组件,并且可以Angular中使用。
剩下是工作就是把它加入到AppModule
的declarations
数组。
hero-detail.component.ts
import { Directive, ElementRef, Injector } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';
@Directive({
selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('heroDetail', elementRef, injector);
}
}
hero-detail.component.ts
@NgModule({
imports: [
BrowserModule,
UpgradeModule
],
declarations: [
HeroDetailDirective,
/* . . . */
]
})
export class AppModule {
ngDoBootstrap() {}
}
升级后的组件是Angular的指令,而不是组件,因为Angular不知道AngularJS将在它下面创建元素。 Angular所知道的是升级后的组件只是一个指令(一个标签),Angular不需要关心组件本身及其子元素。
升级后的组件也可能有输入属性和输出属性,它们是在原AngularJS组件型指令的scope/controller绑定中定义的。 当我们从Angular模板中使用该组件时,我们要使用Angular模板语法来提供这些输入属性和输出属性,但要遵循下列规则:
绑定定义 | 模板语法 | |
---|---|---|
属性(Attribute)绑定 |
|
|
表达式绑定 |
|
|
单向绑定 |
|
|
双向绑定 |
|
用作输入: |
举个例子,假设我们在AngularJS中有一个表示“英雄详情”的组件型指令,它带有一个输入属性和一个输出属性:
hero-detail.component.ts
export const heroDetail = {
bindings: {
hero: '<',
deleted: '&'
},
template: `
<h2>{{$ctrl.hero.name}} details!</h2>
<div><label>id: </label>{{$ctrl.hero.id}}</div>
<button ng-click="$ctrl.onDelete()">Delete</button>
`,
controller: function() {
this.onDelete = () => {
this.deleted(this.hero);
};
}
};
我们可以把这个组件升级到Angular,然后使用Angular的模板语法提供这个输入属性和输出属性:
hero-detail.component.ts
import { Directive, ElementRef, Injector, Input, Output, EventEmitter } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';
import { Hero } from '../hero';
@Directive({
selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent {
@Input() hero: Hero;
@Output() deleted: EventEmitter<Hero>;
constructor(elementRef: ElementRef, injector: Injector) {
super('heroDetail', elementRef, injector);
}
}
container.component.ts
import { Component } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'my-container',
template: `
<h1>Tour of Heroes</h1>
<hero-detail [hero]="hero"
(deleted)="heroDeleted($event)">
</hero-detail>
`
})
export class ContainerComponent {
hero = new Hero(1, 'Windstorm');
heroDeleted(hero: Hero) {
hero.name = 'Ex-' + hero.name;
}
}
把AngularJS的内容投影到Angular组件中
Projecting AngularJS Content into Angular Components

如果我们在AngularJS模板中使用降级后的Angular组件时,可能会需要把模板中的一些内容投影进那个组件。
这也是可能的,虽然在Angular中并没有透传(transclude)这样的东西,但它有一个非常相似的概念,叫做内容投影。
UpgradeModule
也能让这两个特性实现互操作。
Angular的组件通过使用<ng-content>
标签来支持内容投影。下面是这类组件的一个例子:
hero-detail.component.ts
import { Component, Input } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'hero-detail',
template: `
<h2>{{hero.name}}</h2>
<div>
<ng-content></ng-content>
</div>
`
})
export class HeroDetailComponent {
@Input() hero: Hero;
}
当从AngularJS中使用该组件时,我们可以为它提供内容。正如它们将在AngularJS中被透传一样,
它们也在Angular中被投影到了<ng-content>
标签所在的位置:
<div ng-controller="MainController as mainCtrl">
<hero-detail [hero]="mainCtrl.hero">
<!-- Everything here will get projected -->
<p>{{mainCtrl.hero.description}}</p>
</hero-detail>
</div>
当AngularJS的内容被投影到Angular组件中时,它仍然留在“AngularJS王国”中,并被AngularJS框架管理着。
把Angular的内容透传进AngularJS的组件型指令
Transcluding Angular Content into AngularJS Component Directives

就像我们能把AngularJS的内容投影进Angular组件一样,我们也能把Angular的内容透传进AngularJS的组件, 但不管怎样,我们都要使用它们升级过的版本。
如果一个AngularJS组件型指令支持透传,它就会在自己的模板中使用ng-transclude
指令标记出透传到的位置:
hero-detail.component.ts
export const heroDetail = {
bindings: {
hero: '='
},
template: `
<h2>{{$ctrl.hero.name}}</h2>
<div>
<ng-transclude></ng-transclude>
</div>
`
};
该指令还需要启用一个transclude: true
选项。当用AngularJS.5中的组件API定义组件型指令时,该选项默认是开启的。
如果我们升级这个组件,并把它用在Angular中,我们就能把准备透传的内容放进这个组件的标签中。
container.component.ts
import { Component } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'my-container',
template: `
<hero-detail [hero]="hero">
<!-- Everything here will get transcluded -->
<p>{{hero.description}}</p>
</hero-detail>
`
})
export class ContainerComponent {
hero = new Hero(1, 'Windstorm', 'Specific powers of controlling winds');
}
让AngularJS中的依赖可被注入到Angular
Making AngularJS Dependencies Injectable to Angular
当运行一个混合式应用时,我们可能会遇到这种情况:我们需要把某些AngularJS的依赖注入到Angular代码中。
这可能是因为某些业务逻辑仍然在AngularJS服务中,或者需要某些AngularJS的内置服务,比如$location
或$timeout
。
在这些情况下,把一个AngularJS提供商升级到Angular也是有可能的。这就让它将来有可能被注入到Angular代码中的某些地方。
比如,我们可能在AngularJS中有一个名叫HeroesService
的服务:
heroes.service.ts
import { Hero } from '../hero';
export class HeroesService {
get() {
return [
new Hero(1, 'Windstorm'),
new Hero(2, 'Spiderman')
];
}
}
我们可以Angular的工厂提供商(factory provider)升级该服务,
它从AngularJS的$injector
请求服务。Angular依赖的名称由你确定:
我们建议在一个独立的ajs-upgraded-providers.ts
中声明这个工厂提供商,以便把它们都放在一起,这样便于引用、创建新的以及在升级完毕时删除它们。
我们还建议导出heroesServiceFactory
函数,以便AOT编译器可以拿到它们。
ajs-upgraded-providers.ts
import { HeroesService } from './heroes.service';
export function heroesServiceFactory(i: any) {
return i.get('heroes');
}
export const heroesServiceProvider = {
provide: HeroesService,
useFactory: heroesServiceFactory,
deps: ['$injector']
};
app.module.ts
import { heroesServiceProvider } from './ajs-upgraded-providers';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
],
providers: [
heroesServiceProvider
],
/* . . . */
})
export class AppModule {
ngDoBootstrap() {}
}
然后我们可以一个字符串型令牌,把它注入到Angular中:
hero-detail.component.ts
import { Component } from '@angular/core';
import { HeroesService } from './heroes.service';
import { Hero } from '../hero';
@Component({
selector: 'hero-detail',
template: `
<h2>{{hero.id}}: {{hero.name}}</h2>
`
})
export class HeroDetailComponent {
hero: Hero;
constructor(heroes: HeroesService) {
this.hero = heroes.get()[0];
}
}
在这个例子中,我们升级了服务类。当我们注入它时,我们可以使用TypeScript类型注解来获得这些额外的好处。 它没有影响该依赖的处理过程,同时还得到了启用静态类型检查的好处。 任何AngularJS中的服务、工厂和提供商都能被升级 —— 尽管这不是必须的。
让Angular的依赖能被注入到AngularJS中
Making Angular Dependencies Injectable to AngularJS
除了能升级AngularJS依赖之外,我们还能降级Angular的依赖,以便我们能在AngularJS中使用它们。 当我们已经开始把服务移植到Angular或在Angular中创建新服务,但同时还有一些用AngularJS写成的组件时,这会非常有用。
例如,我们可能有一个Angular的Heroes
服务:
heroes.ts
import { Injectable } from '@angular/core';
import { Hero } from '../hero';
@Injectable()
export class Heroes {
get() {
return [
new Hero(1, 'Windstorm'),
new Hero(2, 'Spiderman')
];
}
}
仿照Angular组件,我们通过把该提供商加入NgModule
的providers
列表中来注册它。
app.module.ts
import { Heroes } from './heroes';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
],
providers: [ Heroes ]
})
export class AppModule {
ngDoBootstrap() {}
}
现在,我们使用upgradeAdapter.downgradeNg2Provider()
来把Angular的Heroes
包装成AngularJS的工厂函数,并把这个工厂注册进AngularJS的模块中。
依赖在AngularJS中的名字你可以自己定:
app.module.ts
import { downgradeInjectable } from '@angular/upgrade/static';
angular.module('heroApp', [])
.factory('heroes', downgradeInjectable(Heroes))
.component('heroDetail', heroDetailComponent);
此后,该服务就能被注入到AngularJS代码中的任何地方了:
hero-detail.component.ts
export const heroDetailComponent = {
template: `
<h2>{{$ctrl.hero.id}}: {{$ctrl.hero.name}}</h2>
`,
controller: ['heroes', function(heroes: Heroes) {
this.hero = heroes.get()[0];
}]
};
在混合式应用中使用AOT编译
Using Ahead-of-time compilation with hybrid apps
我们也可以其它Angular应用一样在混合式应用中发挥AOT编译的优势。
对混合式应用的设置过程和预编译章节中所讲的几乎完全一样,不同点在于index.html
和main-aot.ts
中。
我们的index.html
仍然需要script标签来加载AngularJS的文件,因此我们使用AOT编译的index.html
也需要加载那些文件。
复制它们的简单方案是把它们全都添加到copy-dist-files.js
文件中。
我们还要使用UpgradeModule
在启动了模块工厂之后引导一个混合式应用:
app/main-aot.ts
import { platformBrowser } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory).then(platformRef => {
const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.documentElement, ['phonecatApp']);
});
这就是我们为获取Angular应用的AOT优势所要做的一切。
AOT元数据收集器不能检测到父类原型中的生命周期钩子方法,因此为了让升级后的组件正常工作,我们要在升级后的组件类中实现生命周期钩子,
并且把它们转发给UpgradeComponent
的父类。
在Angular和AngularJS之间划分路由
Dividing routes between Angular and AngularJS
升级中的另一个重要部分是升级路由。 我们固然可以升级整个应用但仍然使用AngularJS的路由器,然后一举把所有路由迁移过去。 但更好地方式是在升级每个路由时逐个把它们迁移过去。
让两个版本的路由器并存的第一步就是为每个路由器添加一个包含单一出口的根组件。
AngularJS将会使用ng-view
,而Angular将会使用router-outlet
。
当使用其中的一个路由器时,另一个路由出口就是空白的。
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<router-outlet></router-outlet>
<div ng-view></div>
`,
})
export class AppComponent { }
我们要在index.html
的body
中使用该组件来代替AngularJS的组件。
app.component.ts (body)
<body>
<my-app>Loading...</my-app>
</body>
接下来,我们用标准方式同时声明AngularJS和Angular的路由:
app.module.ts (AngularJS route)
$routeProvider
.when('/villain', { template: '<villain-detail></villain-detail>' });
hero.module.ts (Angular route)
RouterModule.forChild([
{ path: 'hero', children: [
{ path: '', component: HeroDetailComponent },
] },
])
在app.module.ts
中,我们需要把AppComponent
添加到declarations
和bootstrap
数组中。
接下来,我们配置路由器本身。 我们要在Angular中使用hash navigation,因为我们也要在AngularJS中使用这种方式。
最后,最重要的是,我们要使用一个自定义的UrlHandlingStrategy
,它会告诉Angular路由器应该渲染(且只渲染)哪个路由。
app.module.ts (router config)
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
import { RouterModule, UrlHandlingStrategy, UrlTree } from '@angular/router';
import { AppComponent } from './app.component';
class HybridUrlHandlingStrategy implements UrlHandlingStrategy {
// use only process the `/hero` url
shouldProcessUrl(url: UrlTree) { return url.toString().startsWith('/hero'); }
extract(url: UrlTree) { return url; }
merge(url: UrlTree, whole: UrlTree) { return url; }
}
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HeroModule,
RouterModule.forRoot([])
],
providers: [
// use hash location strategy
{ provide: LocationStrategy, useClass: HashLocationStrategy },
// use custom url handling strategy
{ provide: UrlHandlingStrategy, useClass: HybridUrlHandlingStrategy }
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
就这样!现在我们可以同时运行这两个路由器了。
PhoneCat升级教程
PhoneCat Upgrade Tutorial
在本节和下节中,我们将看一个完整的例子,它使用upgrade
模块准备和升级了一个应用程序。
该应用就是来自原AngularJS教程中的Angular PhoneCat。
那是我们很多人当初开始Angular探险之旅的起点。
现在,我们来看看如何把该应用带入Angular的美丽新世界。
这期间,我们将学到如何在实践中应用准备指南中列出的那些重点步骤: 我们先让该应用向Angular看齐,然后为它引入SystemJS模块加载器和TypeScript。
要跟随本教程,请先把angular-phonecat仓库克隆到本地,并跟我们一起应用这些步骤。
在项目结构方面,我们工作的起点是这样的:
这确实是一个很好地起点。特别是,该结构遵循了AngularJS 风格指南, 要想成功升级,这是一个很重要的准备步骤。
-
每个组件、服务和过滤器都在它自己的源文件中 —— 就像单一规则所要求的。
-
core
、phone-detail
和phone-list
模块都在它们自己的子目录中。那些子目录除了包含HTML模板之外,还包含JavaScript代码,它们共同完成一个特性。 这是按特性分目录的结构 和模块化规则所要求的。 -
单元测试都和应用代码在一起,它们很容易找到。就像规则 组织测试文件中要求的那样。
切换到TypeScript
Switching to TypeScript
因为我们将使用TypeScript编写Angular的代码,所以在开始升级之前,我们把TypeScript的编译器设置好是很合理的。
我们还将开始逐步淘汰Bower包管理器,换成我们更喜欢的NPM。后面我们将使用NPM来安装新的依赖包,并最终从项目中移除Bower。
让我们先把TypeScript包安装到项目中。
npm i typescript --save-dev
我们还要把用来运行TypeScript编译器tsc
和typings
工具的脚本添加到package.json
中:
package.json
{
"scripts": {}
}
现在我们可以使用typings工具来安装AngularJS和Jasmine单元测试框架的类型定义文件。
npm install @types/jasmine @types/angular @types/angular-animate @types/angular-cookies @types/angular-mocks @types/angular-resource @types/angular-route @types/angular-sanitize --save-dev
我们还应该配置TypeScript编译器,以便它能理解我们的项目结构。我们要往项目目录下添加一个tsconfig.json
文件,
就像在搭建本地开发环境中做过的那样。它将告诉TypeScript编译器,该如何编译我们的源文件。
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true
}
}
我们告诉TypeScript编译器,把TypeScript文件转换成ES5代码,并打包进CommonJS模块中。
我们现在可以从命令行启动TypeScript编译器。它将监控.ts
源码文件,并随时把它们编译成JavaScript。
然后这些编译出的.js
文件被SystemJS加载到浏览器中。当我们继续往前走的时候,这个过程将在后台持续运行。
npm run tsc:w
我们要做的下一件事是把JavaScript文件转换成TypeScript文件。
由于TypeScript是ECMAScript 2015的一个超集,而ES2015又是ECMAScript 5的超集,所以我们可以简单的把文件的扩展名从.js
换成.ts
,
它们还是会像以前一样工作。由于TypeScript编译器仍在运行,它会为每一个.ts
文件生成对应的.js
文件,而真正运行的是编译后的.js
文件。
如果你用npm start
开启了本项目的HTTP服务器,你会在浏览器中看到一个功能完好的应用。
有了TypeScript,我们就可以从它的一些特性中获益了。此语言可以为AngularJS应用提供很多价值。
首先,TypeScript是一个ES2015的超集。任何以前用ES5写的程序(就像PhoneCat范例)都可以开始通过TypeScript
纳入那些添加到ES2015中的新特性。
这包括let
、const
、箭头函数、函数默认参数以及解构(destructure)赋值。
我们能做的另一件事就是把类型安全添加到代码中。这实际上已经部分完成了,因为我们已经安装了AngularJS的类型定义。 当我们正确调用AngularJS的API时,TypeScript会帮我们检查它 —— 比如往Angular模块中注册组件。
我们还能开始把类型注解添加到自己的代码中,来从TypeScript的类型系统中获得更多帮助。
比如,我们可以给checkmark
过滤器加上注解,表明它期待一个boolean
类型的参数。
这可以更清楚的表明此过滤器打算做什么
app/core/checkmark/checkmark.filter.ts
angular.
module('core').
filter('checkmark', function() {
return function(input: boolean) {
return input ? '\u2713' : '\u2718';
};
});
在Phone
服务中,我们可以明确的把$resource
服务声明为angular.resource.IResourceService
,一个AngularJS类型定义提供的类型。
app/core/phone/phone.service.ts
angular.
module('core.phone').
factory('Phone', ['$resource',
function($resource: angular.resource.IResourceService) {
return $resource('phones/:phoneId.json', {}, {
query: {
method: 'GET',
params: {phoneId: 'phones'},
isArray: true
}
});
}
]);
我们可以在应用的路由配置中使用同样的技巧,那里我们用到了location和route服务。 一旦给它们提供了类型信息,TypeScript就能检查我们是否在用类型的正确参数来调用它们了。
app/app.config.ts
angular.
module('phonecatApp').
config(['$locationProvider', '$routeProvider',
function config($locationProvider: angular.ILocationProvider,
$routeProvider: angular.route.IRouteProvider) {
$locationProvider.hashPrefix('!');
$routeProvider.
when('/phones', {
template: '<phone-list></phone-list>'
}).
when('/phones/:phoneId', {
template: '<phone-detail></phone-detail>'
}).
otherwise('/phones');
}
]);
我们用typings工具安装的这个AngularJS.x类型定义文件 并不是由Angular开发组维护的,但它也已经足够全面了。借助这些类型定义的帮助,它可以为AngularJS.x程序加上全面的类型注解。
如果我们想这么做,那么在tsconfig.json
中启用noImplicitAny
配置项就是一个好主意。
这样,如果遇到什么还没有类型注解的代码,TypeScript编译器就会显示一个警告。
我们可以用它作为指南,告诉我们现在与一个完全类型化的项目距离还有多远。
我们能用的另一个TypeScript特性是类。具体来讲,我们可以把控制器转换成类。 这种方式下,我们离成为Angular组件类就又近了一步,它会令我们的升级之路变得更简单。
AngularJS期望控制器是一个构造函数。这实际上就是ES2015/TypeScript中的类, 这也就意味着只要我们把一个类注册为组件控制器,AngularJS就会愉快的使用它。
新的“电话列表(phone list)”组件控制器类是这样的:
app/phone-list/phone-list.component.ts
class PhoneListController {
phones: any[];
orderProp: string;
query: string;
static $inject = ['Phone'];
constructor(Phone: any) {
this.phones = Phone.query();
this.orderProp = 'age';
}
}
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'phone-list/phone-list.template.html',
controller: PhoneListController
});
以前在控制器函数中实现的一切,现在都改由类的构造函数来实现了。类型注入注解通过静态属性$inject
被附加到了类上。在运行时,它们变成了PhoneListController.$inject
。
该类还声明了另外三个成员:电话列表、当前排序键的名字和搜索条件。 这些东西我们以前就加到了控制器上,只是从来没有在任何地方显式定义过它们。最后一个成员从未真正在TypeScript代码中用过, 因为它只是在模板中被引用过。但为了清晰起见,我们还是应该定义出此控制器应有的所有成员。
在电话详情控制器中,我们有两个成员:一个是用户正在查看的电话,另一个是正在显示的图像:
app/phone-detail/phone-detail.component.ts
class PhoneDetailController {
phone: any;
mainImageUrl: string;
static $inject = ['$routeParams', 'Phone'];
constructor($routeParams: angular.route.IRouteParamsService, Phone: any) {
let phoneId = $routeParams['phoneId'];
this.phone = Phone.get({phoneId}, (phone: any) => {
this.setImage(phone.images[0]);
});
}
setImage(imageUrl: string) {
this.mainImageUrl = imageUrl;
}
}
angular.
module('phoneDetail').
component('phoneDetail', {
templateUrl: 'phone-detail/phone-detail.template.html',
controller: PhoneDetailController
});
这已经让我们的控制器代码看起来更像Angular了。我们的准备工作做好了,可以引进Angular到项目中了。
如果项目中有任何AngularJS的服务,它们也是转换成类的优秀候选人,像控制器一样,它们也是构造函数。
但是在本项目中,我们只有一个Phone
工厂,这有点特别,因为它是一个ngResource
工厂。
所以我们不会在准备阶段中处理它,而是在下一节中直接把它转换成Angular服务。
安装Angular
Installing Angular
我们已经完成了准备工作,接下来就开始把PhoneCat升级到Angular。 我们将在Angular升级模块的帮助下增量式的完成此项工作。 等我们完成的那一刻,就能把AngularJS从项目中完全移除了,但其中的关键是在不破坏此程序的前提下一小块一小块的完成它。
该项目还包含一些动画,在此指南的当前版本我们先不升级它,等到后面的发行版再改。
我们来使用SystemJS模块加载器把Angular安装到项目中。 看看搭建本地开发环境中的指南,并从那里获得如下配置:
-
把Angular和其它新依赖添加到
package.json
中 -
把SystemJS的配置文件
systemjs.config.js
添加到项目的根目录。
这些完成之后,就运行:
npm install
我们可以通过index.html
来把Angular的依赖快速加载到应用中,
但首先,我们得做一些目录结构调整。这是因为我们正准备从node_modules
中加载文件,然而目前项目中的每一个文件都是从/app
目录下加载的。
把app/index.html
移入项目的根目录,然后把package.json
中的开发服务器根目录也指向项目的根目录,而不再是app
目录:
package.json (start script)
{
"scripts": {
"start": "http-server -a localhost -p 8000 -c-1 ./"
}
}
现在,我们能把项目根目录下的每一样东西发给浏览器了。但我们不想为了适应开发环境中的设置,被迫修改应用代码中用到的所有图片和数据的路径。因此,我们往index.html
中添加一个<base>
标签,它将导致各种相对路径被解析回/app
目录:
index.html
<base href="/app/">
现在我们可以通过SystemJS加载Angular了。我们将把Angular的填充库(polyfills)
和SystemJS的配置加到<head>
区的末尾,然后,我们就用System.import
来加载实际的应用:
index.html
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script>
System.import('/app');
</script>
我们还需要对环境设置期间安装的systemjs.config.js
文件做一些调整。
我们要在通过SystemJS加载期间为浏览器指出项目的根在哪里,而不再使用<base>
URL。
我们还要通过npm install @angular/upgrade --save
来安装upgrade
包,并为@angular/upgrade/static
包添加一个映射。
systemjs.config.js
System.config({
paths: {
// paths serve as alias
'npm:': '/node_modules/'
},
map: {
app: '/app',
/* . . . */
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
/* . . . */
},
创建AppModule
Creating the AppModule
现在,创建一个名叫AppModule
的根NgModule
类。
我们已经有了一个名叫app.module.ts
的文件,其中存放着AngularJS的模块。
把它改名为app.module.ng1.ts
,同时也要在index.html
中更新对应的脚本名。
文件的内容保留:
app.module.ajs.ts
'use strict';
// Define the `phonecatApp` AngularJS module
angular.module('phonecatApp', [
'ngAnimate',
'ngRoute',
'core',
'phoneDetail',
'phoneList',
]);
然后创建一个新的app.module.ts
文件,其中是一个最小化的NgModule
类:
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
imports: [
BrowserModule,
],
})
export class AppModule {
}
引导PhoneCat的1+2混合式应用
Bootstrapping a hybrid PhoneCat
接下来,我们把该应用程序引导改装为一个同时支持AngularJS和Angular的混合式应用。 然后,就能开始把这些不可分割的小块转换到Angular了。
要引导一个混合式应用程序,
我们首先需要在AppModule
中导入`UpgradeModule,并覆盖它的启动方法:
app/app.module.ts
import { UpgradeModule } from '@angular/upgrade/static';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
],
})
export class AppModule {
ngDoBootstrap() {}
}
我们的应用现在是使用宿主页面中附加到<html>
元素上的ng-app
指令引导的。
但在Angular中,它不再工作了。我们得切换成JavaScript驱动的引导方式。
所以,从index.html
中移除ng-app
属性,并把这些加载main.ts
中。
在systemjs.config.js
中已经将此文件配置为应用的入口,所以它已被浏览器所加载。
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => {
const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.documentElement, ['phonecatApp']);
});
这里使用的参数是应用的根元素(也就是以前我们放ng-app
的元素),和我们准备加载的AngularJS.x模块。
由于我们是通过UpgradeAdapter
引导应用的,所以实际在运行的应用实际上是一个混合体。
我们现在同时运行着AngularJS和Angular。漂亮!不过我们还没有运行什么实际的Angular组件,接下来我们就做这件事。
为何要声明angular为angular.IAngularStatic?
Why declare angular as angular.IAngularStatic?
@types/angular
声明为UMD模块,根据UMD类型
的工作方式,一旦你在文件中有一条ES6的import
语句,所有的UMD类型化的模型必须都通过import
语句导入,
而是不是全局可用。
AngularJS是日前是通过index.html
中的script标签加载,这意味着整个应用是作为一个全局变量进行访问的,
使用同一个angular
变量的实例。
但如果我们使用import * as angular from 'angular'
,我还需要彻底修改AngularJS应用中加载每个文件的方式,
确保AngularJS应用被正确加载。
这需要相当多的努力,通常也不值得去做,特别是我们的应用正在朝着Angular前进。
但如果我们声明angular
为angular.IAngularStatic
,指明它是一个全局变量,
仍然可以获得全面的类型支持。
升级Phone
服务
Upgrading the Phone service
我们要移植到Angular的第一块是Phone
工厂(位于app/js/core/phones.factory.ts
),
并且让它能帮助控制器从服务器上加载电话信息。目前,它是用ngResource
实现的,我们用它做两件事:
-
把所有电话的列表加载到电话列表组件中。
-
把一台电话的详情加载到电话详情组件中。
我们可以用Angular的服务类来替换这个实现,而把控制器继续留在AngularJS的地盘上。
在这个新版本中,我们导入了Angular的HTTP模块,并且用它的Http
服务替换掉NgResource
。
再次打开app.module.ts
文件,导入并把HttpModule
添加到AppModule
的imports
数组中:
app.module.ts
import { HttpModule } from '@angular/http';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
],
})
export class AppModule {
ngDoBootstrap() {}
}
现在,我们已经准备好了升级Phones
服务本身。我们将为phone.service.ts
文件中基于ngResource的服务加上@Injectable
装饰器:
app/core/phone/phone.service.ts (skeleton)
@Injectable()
export class Phone {
/* . . . */
}
@Injectable
装饰器将把一些依赖注入相关的元数据附加到该类上,让Angular知道它的依赖信息。
就像在依赖注入指南中描述过的那样,
这是一个标记装饰器,我们要把它用在那些没有其它Angular装饰器,并且自己有依赖注入的类上。
在它的构造函数中,该类期待一个Http
服务。Http
服务将被注入进来并存入一个私有字段。
然后该服务在两个实例方法中被使用到,一个加载所有电话的列表,另一个加载一台指定电话的详情:
@Injectable()
export class Phone {
constructor(private http: Http) { }
query(): Observable<PhoneData[]> {
return this.http.get(`phones/phones.json`)
.map((res: Response) => res.json());
}
get(id: string): Observable<PhoneData> {
return this.http.get(`phones/${id}.json`)
.map((res: Response) => res.json());
}
}
该方法现在返回一个Phone
类型或Phone[]
类型的可观察对象(Observable)。
这是一个我们从未用过的类型,因此我们得为它新增一个简单的接口:
app/core/phone/phone.service.ts (interface)
export interface PhoneData {
name: string;
snippet: string;
images: string[];
}
@angular/upgrade/static
有一个downgradeInjectable
方法,可以使Angular服务在AngularJS的代码中可用。
使用它来插入Phone
服务:
app/core/phone/phone.service.ts (downgrade)
declare var angular: angular.IAngularStatic;
import { downgradeInjectable } from '@angular/upgrade/static';
/* . . . */
@Injectable()
export class Phone {
/* . . . */
}
angular.module('core.phone')
.factory('phone', downgradeInjectable(Phone));
最终,该类的全部代码如下:
app/core/phone/phone.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';
declare var angular: angular.IAngularStatic;
import { downgradeInjectable } from '@angular/upgrade/static';
import 'rxjs/add/operator/map';
export interface PhoneData {
name: string;
snippet: string;
images: string[];
}
@Injectable()
export class Phone {
constructor(private http: Http) { }
query(): Observable<PhoneData[]> {
return this.http.get(`phones/phones.json`)
.map((res: Response) => res.json());
}
get(id: string): Observable<PhoneData> {
return this.http.get(`phones/${id}.json`)
.map((res: Response) => res.json());
}
}
angular.module('core.phone')
.factory('phone', downgradeInjectable(Phone));
注意,我们单独导入了RxJS Observable
中的map
操作符。
我们需要对想用的所有RxJS操作符这么做,因为Angular默认不会加载所有RxJS操作符。
这个新的Phone
服务具有和老的基于ngResource
的服务相同的特性。
因为它是Angular服务,我们通过NgModule
的providers
数组来注册它:
app.module.ts
import { Phone } from './core/phone/phone.service';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
],
providers: [
Phone,
]
})
export class AppModule {
ngDoBootstrap() {}
}
现在,我们正在用SystemJS加载phone.service.ts
,我们应该从index.html
中移除该服务的<script>
标签。
这也是我们在升级所有组件时将会做的事。在从AngularJS向2升级的同时,我们也把代码从脚本移植为模块。
这时,我们可以把两个控制器从使用老的服务切换成使用新的。我们像降级过的phones
工厂一样$inject
它,
但它实际上是一个Phones
类的实例,并且我们可以据此注解它的类型:
app/phone-list/phone-list.component.ts
declare var angular: angular.IAngularStatic;
import { Phone, PhoneData } from '../core/phone/phone.service';
class PhoneListController {
phones: PhoneData[];
orderProp: string;
static $inject = ['phone'];
constructor(phone: Phone) {
phone.query().subscribe(phones => {
this.phones = phones;
});
this.orderProp = 'age';
}
}
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'app/phone-list/phone-list.template.html',
controller: PhoneListController
});
app/phone-detail/phone-detail.component.ts
declare var angular: angular.IAngularStatic;
import { Phone, PhoneData } from '../core/phone/phone.service';
class PhoneDetailController {
phone: PhoneData;
mainImageUrl: string;
static $inject = ['$routeParams', 'phone'];
constructor($routeParams: angular.route.IRouteParamsService, phone: Phone) {
let phoneId = $routeParams['phoneId'];
phone.get(phoneId).subscribe(data => {
this.phone = data;
this.setImage(data.images[0]);
});
}
setImage(imageUrl: string) {
this.mainImageUrl = imageUrl;
}
}
angular.
module('phoneDetail').
component('phoneDetail', {
templateUrl: 'phone-detail/phone-detail.template.html',
controller: PhoneDetailController
});
这里的两个AngularJS控制器在使用Angular的服务!控制器不需要关心这一点,尽管实际上该服务返回的是可观察对象(Observable),而不是承诺(Promise)。 无论如何,我们达到的效果都是把服务移植到Angular,而不用被迫移植组件来使用它。
我们也能使用Observable
的toPromise
方法来在服务中把这些可观察对象转变成承诺,以进一步减小组件控制器中需要修改的代码量。
升级组件
Upgrading Components
接下来,我们把AngularJS的控制器升级成Angular的组件。我们每次升级一个,同时仍然保持应用运行在混合模式下。 在做转换的同时,我们还将自定义首个Angular管道。
让我们先看看电话列表组件。它目前包含一个TypeScript控制器类和一个组件定义对象。重命名控制器类,
并把AngularJS的组件定义对象更换为Angular @Component
装饰器,这样我们就把它变形为Angular
的组件了。然后,我们还从类中移除静态$inject
属性。
app/phone-list/phone-list.component.ts
import { Component } from '@angular/core';
import { Phone, PhoneData } from '../core/phone/phone.service';
@Component({
selector: 'phone-list',
templateUrl: 'phone-list.template.html'
})
export class PhoneListComponent {
phones: PhoneData[];
query: string;
orderProp: string;
constructor(phone: Phone) {
phone.query().subscribe(phones => {
this.phones = phones;
});
this.orderProp = 'age';
}
/* . . . */
}
selector
属性是一个CSS选择器,用来定义组件应该被放在页面的哪。在AngularJS,我们基于组件名字来匹配,
但是在Angular中,我们要有一个专门指定的选择器。本组件将会对应元素名字phone-list
,和AngularJS版本一样。
现在,我们还需要将组件的模版也转换为Angular语法。在搜索控件中,我们要为把AngularJS的$ctrl
表达式替换成Angular的双向绑定语法[(ngModel)]
:
app/phone-list/phone-list.template.html (search controls)
<p>
Search:
<input [(ngModel)]="query" />
</p>
<p>
Sort by:
<select [(ngModel)]="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
</p>
我们需要把列表中的ng-repeat
替换为*ngFor
以及它的let var of iterable
语法,
该语法在模板语法指南中讲过。
对于图片,我们可以把img
标签的ng-src
替换为一个标准的src
属性(property)绑定。
app/phone-list/phone-list.template.html (phones)
<ul class="phones">
<li *ngFor="let phone of getPhones()"
class="thumbnail phone-list-item">
<a href="/#!/phones/{{phone.id}}" class="thumb">
<img [src]="phone.imageUrl" [alt]="phone.name" />
</a>
<a href="/#!/phones/{{phone.id}}" class="name">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
Angular中没有filter
或orderBy
过滤器
No Angular filter or orderBy filters
Angular中并不存在AngularJS中内置的filter
和orderBy
过滤器。
所以我们得自己实现进行过滤和排序。
我们把filter
和orderBy
过滤器改成绑定到控制器中的getPhones()
方法,通过该方法,组件本身实现了过滤和排序逻辑。
app/phone-list/phone-list.component.ts
getPhones(): PhoneData[] {
return this.sortPhones(this.filterPhones(this.phones));
}
private filterPhones(phones: PhoneData[]) {
if (phones && this.query) {
return phones.filter(phone => {
let name = phone.name.toLowerCase();
let snippet = phone.snippet.toLowerCase();
return name.indexOf(this.query) >= 0 || snippet.indexOf(this.query) >= 0;
});
}
return phones;
}
private sortPhones(phones: PhoneData[]) {
if (phones && this.orderProp) {
return phones
.slice(0) // Make a copy
.sort((a, b) => {
if (a[this.orderProp] < b[this.orderProp]) {
return -1;
} else if ([b[this.orderProp] < a[this.orderProp]]) {
return 1;
} else {
return 0;
}
});
}
return phones;
}
现在我们需要降级我们的Angular组件,这样我们就可以在AngularJS中使用它。
我们需要注册一个phoneList
指令,而不是注册一个组件,它是一个降级版的Angular组件。
强制类型转换as angular.IDirectiveFactory
告诉TypeScript编译器downgradeComponent
方法
的返回值是一个指令工厂。
app/phone-list/phone-list.component.ts
declare var angular: angular.IAngularStatic;
import { downgradeComponent } from '@angular/upgrade/static';
/* . . . */
@Component({
selector: 'phone-list',
templateUrl: 'phone-list.template.html'
})
export class PhoneListComponent {
/* . . . */
}
angular.module('phoneList')
.directive(
'phoneList',
downgradeComponent({component: PhoneListComponent}) as angular.IDirectiveFactory
);
新的PhoneListComponent
使用Angular的ngModel
指令,它位于FormsModule
中。
把FormsModule
添加到NgModule
的imports
中,并声明新的PhoneListComponent
组件,
最后由我们把它降级了,添加到entryComponents
:
app.module.ts
import { FormsModule } from '@angular/forms';
import { PhoneListComponent } from './phone-list/phone-list.component';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
FormsModule,
],
declarations: [
PhoneListComponent,
],
entryComponents: [
PhoneListComponent,
})
export class AppModule {
ngDoBootstrap() {}
}
从index.html
中移除电话列表组件的<script>标签。
现在,剩下的phone-detail.component.ts
文件变成了这样:
app/phone-detail/phone-detail.component.ts
declare var angular: angular.IAngularStatic;
import { downgradeComponent } from '@angular/upgrade/static';
import { Component } from '@angular/core';
import { Phone, PhoneData } from '../core/phone/phone.service';
import { RouteParams } from '../ajs-upgraded-providers';
@Component({
selector: 'phone-detail',
templateUrl: './phone-detail.template.html',
})
export class PhoneDetailComponent {
phone: PhoneData;
mainImageUrl: string;
constructor(routeParams: RouteParams, phone: Phone) {
phone.get(routeParams['phoneId']).subscribe(phone => {
this.phone = phone;
this.setImage(phone.images[0]);
});
}
setImage(imageUrl: string) {
this.mainImageUrl = imageUrl;
}
}
angular.module('phoneDetail')
.directive(
'phoneDetail',
downgradeComponent({component: PhoneDetailComponent}) as angular.IDirectiveFactory
);
这和电话列表组件很相似。
这里的窍门在于@Inject
装饰器,它标记出了$routeParams
依赖。
AngularJS注入器具有AngularJS路由器的依赖,叫做$routeParams
。
它被注入到了PhoneDetails
中,但PhoneDetails
现在还是一个AngularJS控制器。
我们应该把它注入到新的PhoneDetailsComponent
中。
不幸的是,AngularJS的依赖不会自动在Angular的组件中可用。
我们必须使用工厂提供商(factory provider)
来把$routeParams
包装成Angular的服务提供商。
新建一个名叫ajs-upgraded-providers.ts
的文件,并且在app.module.ts
中导入它:
app/ajs-upgraded-providers.ts
export abstract class RouteParams {
[key: string]: string;
}
export function routeParamsFactory(i: any) {
return i.get('$routeParams');
}
export const routeParamsProvider = {
provide: RouteParams,
useFactory: routeParamsFactory,
deps: ['$injector']
};
app/app.module.ts ($routeParams)
import { routeParamsProvider } from './ajs-upgraded-providers';
providers: [
Phone,
routeParamsProvider
]
我们现在也要把该组件的模板转变成Angular的语法。 这里是它完整的新模板:
app/phone-detail/phone-detail.template.html
<div *ngIf="phone">
<div class="phone-images">
<img [src]="img" class="phone"
[ngClass]="{selected: img === mainImageUrl}"
*ngFor="let img of phone.images" />
</div>
<h1>{{phone.name}}</h1>
<p>{{phone.description}}</p>
<ul class="phone-thumbs">
<li *ngFor="let img of phone.images">
<img [src]="img" (click)="setImage(img)" />
</li>
</ul>
<ul class="specs">
<li>
<span>Availability and Networks</span>
<dl>
<dt>Availability</dt>
<dd *ngFor="let availability of phone.availability">{{availability}}</dd>
</dl>
</li>
<li>
<span>Battery</span>
<dl>
<dt>Type</dt>
<dd>{{phone.battery?.type}}</dd>
<dt>Talk Time</dt>
<dd>{{phone.battery?.talkTime}}</dd>
<dt>Standby time (max)</dt>
<dd>{{phone.battery?.standbyTime}}</dd>
</dl>
</li>
<li>
<span>Storage and Memory</span>
<dl>
<dt>RAM</dt>
<dd>{{phone.storage?.ram}}</dd>
<dt>Internal Storage</dt>
<dd>{{phone.storage?.flash}}</dd>
</dl>
</li>
<li>
<span>Connectivity</span>
<dl>
<dt>Network Support</dt>
<dd>{{phone.connectivity?.cell}}</dd>
<dt>WiFi</dt>
<dd>{{phone.connectivity?.wifi}}</dd>
<dt>Bluetooth</dt>
<dd>{{phone.connectivity?.bluetooth}}</dd>
<dt>Infrared</dt>
<dd>{{phone.connectivity?.infrared | checkmark}}</dd>
<dt>GPS</dt>
<dd>{{phone.connectivity?.gps | checkmark}}</dd>
</dl>
</li>
<li>
<span>Android</span>
<dl>
<dt>OS Version</dt>
<dd>{{phone.android?.os}}</dd>
<dt>UI</dt>
<dd>{{phone.android?.ui}}</dd>
</dl>
</li>
<li>
<span>Size and Weight</span>
<dl>
<dt>Dimensions</dt>
<dd *ngFor="let dim of phone.sizeAndWeight?.dimensions">{{dim}}</dd>
<dt>Weight</dt>
<dd>{{phone.sizeAndWeight?.weight}}</dd>
</dl>
</li>
<li>
<span>Display</span>
<dl>
<dt>Screen size</dt>
<dd>{{phone.display?.screenSize}}</dd>
<dt>Screen resolution</dt>
<dd>{{phone.display?.screenResolution}}</dd>
<dt>Touch screen</dt>
<dd>{{phone.display?.touchScreen | checkmark}}</dd>
</dl>
</li>
<li>
<span>Hardware</span>
<dl>
<dt>CPU</dt>
<dd>{{phone.hardware?.cpu}}</dd>
<dt>USB</dt>
<dd>{{phone.hardware?.usb}}</dd>
<dt>Audio / headphone jack</dt>
<dd>{{phone.hardware?.audioJack}}</dd>
<dt>FM Radio</dt>
<dd>{{phone.hardware?.fmRadio | checkmark}}</dd>
<dt>Accelerometer</dt>
<dd>{{phone.hardware?.accelerometer | checkmark}}</dd>
</dl>
</li>
<li>
<span>Camera</span>
<dl>
<dt>Primary</dt>
<dd>{{phone.camera?.primary}}</dd>
<dt>Features</dt>
<dd>{{phone.camera?.features?.join(', ')}}</dd>
</dl>
</li>
<li>
<span>Additional Features</span>
<dd>{{phone.additionalFeatures}}</dd>
</li>
</ul>
</div>
这里有几个值得注意的改动:
-
我们从所有表达式中移除了
$ctrl.
前缀。 -
正如我们在电话列表中做过的那样,我们把
ng-src
替换成了标准的src
属性绑定。 -
我们在
ng-class
周围使用了属性绑定语法。虽然Angular中有一个 和AngularJS中非常相似的ngClass
指令, 但是它的值不会神奇的作为表达式进行计算。在Angular中,模板中的属性(Attribute)值总是被作为 属性(Property)表达式计算,而不是作为字符串字面量。 -
我们把
ng-repeat
替换成了*ngFor
。 -
我们把
ng-click
替换成了一个到标准click
事件的绑定。 -
我们把整个模板都包裹进了一个
ngIf
中,这导致只有当存在一个电话时它才会渲染。我们必须这么做, 是因为组件首次加载时我们还没有phone
变量,这些表达式就会引用到一个不存在的值。 和AngularJS不同,当我们尝试引用未定义对象上的属性时,Angular中的表达式不会默默失败。 我们必须明确指出这种情况是我们所期望的。
把PhoneDetailComponent
组件添加到NgModule
的declarations和entryComponents中:
app.module.ts
import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
FormsModule,
],
declarations: [
PhoneListComponent,
PhoneDetailComponent,
],
entryComponents: [
PhoneListComponent,
PhoneDetailComponent
],
providers: [
Phone,
routeParamsProvider
]
})
export class AppModule {
ngDoBootstrap() {}
}
我们现在应该从index.html
中移除电话详情组件的<script>。
添加CheckmarkPipe
Add the CheckmarkPipe
AngularJS指令中有一个checkmark
过滤器,我们把它转换成Angular的管道。
没有什么升级方法能把过滤器转换成管道。
但我们也并不需要它。
把过滤器函数转换成等价的Pipe类非常简单。
实现方式和以前一样,但把它们包装进transform
方法中就可以了。
把该文件改名成checkmark.pipe.ts
,以符合Angular中的命名约定:
app/core/checkmark/checkmark.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'checkmark'})
export class CheckmarkPipe implements PipeTransform {
transform(input: boolean) {
return input ? '\u2713' : '\u2718';
}
}
当我们做这个修改时,也要同时从core
模块文件中移除对该过滤器的注册。该模块的内容变成了:
app.module.ts
import { CheckmarkPipe } from './core/checkmark/checkmark.pipe';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
FormsModule,
],
declarations: [
PhoneListComponent,
PhoneDetailComponent,
CheckmarkPipe
],
entryComponents: [
PhoneListComponent,
PhoneDetailComponent
],
providers: [
Phone,
routeParamsProvider
]
})
export class AppModule {
ngDoBootstrap() {}
}
对混合式应用做AOT编译
AoT compile the hybrid app
要在混合式应用中使用AOT编译,我们首先要像其它Angular应用一样设置它,就像AOT编译一章所讲的那样。
然后,我们就要修改main-aot.ts
的引导代码,也通过UpgradeModule
来引导AngularJS应用:
app/main-aot.ts
import { platformBrowser } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory).then(platformRef => {
const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.documentElement, ['phonecatApp']);
});
我们还要把在index.html
中已经用到的所有AngularJS文件加载到aot/index.html
中:
aot/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<base href="/app/">
<title>Google Phone Gallery</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="app.animations.css" />
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://code.angularjs.org/1.5.5/angular.js"></script>
<script src="https://code.angularjs.org/1.5.5/angular-animate.js"></script>
<script src="https://code.angularjs.org/1.5.5/angular-resource.js"></script>
<script src="https://code.angularjs.org/1.5.5/angular-route.js"></script>
<script src="app.module.ajs.js"></script>
<script src="app.config.js"></script>
<script src="app.animations.js"></script>
<script src="core/core.module.js"></script>
<script src="core/phone/phone.module.js"></script>
<script src="phone-list/phone-list.module.js"></script>
<script src="phone-detail/phone-detail.module.js"></script>
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.min.js"></script>
<script>window.module = 'aot';</script>
</head>
<body>
<div class="view-container">
<div ng-view class="view-frame"></div>
</div>
</body>
<script src="/dist/build.js"></script>
</html>
这些文件要带着相应的填充库复制到一起。应用运行时需要的文件,比如电话列表.json
和图片,也需要复制过去。
通过npm install fs-extra --save-dev
安装fs-extra
可以更好的复制文件,并且把copy-dist-files.js
文件改成这样:
copy-dist-files.js
var fsExtra = require('fs-extra');
var resources = [
// polyfills
'node_modules/core-js/client/shim.min.js',
'node_modules/zone.js/dist/zone.min.js',
// css
'app/app.css',
'app/app.animations.css',
// images and json files
'app/img/',
'app/phones/',
// app files
'app/app.module.ajs.js',
'app/app.config.js',
'app/app.animations.js',
'app/core/core.module.js',
'app/core/phone/phone.module.js',
'app/phone-list/phone-list.module.js',
'app/phone-detail/phone-detail.module.js'
];
resources.map(function(sourcePath) {
var destPath = `aot/${sourcePath}`;
fsExtra.copySync(sourcePath, destPath);
});
这就是想要在升级应用期间AOT编译所需的一切!
切换到Angular路由器和引导程序
Adding The Angular Router And Bootstrap
此刻,我们已经把所有AngularJS的组件替换成了它们在Angular中的等价物,不过我们仍然在AngularJS路由器中使用它们。
大多数AngularJS应用都有多组路由,而如果能每次迁移一个路由就会非常有用。
让我们开始把最初的/
和/phones
路由迁移到Angular,但把/phones/:phoneId
留在AngularJS路由器中。
添加Angular路由器
Add the Angular router
Angular有一个全新的路由器。
像所有的路由器一样,它需要在UI中指定一个位置来显示路由的视图。
在Angular中,它是<router-outlet>
,并位于应用组件树顶部的根组件中。
我们还没有这样一个根组件,因为该应用仍然是像一个AngularJS应用那样被管理的。
创建新的app.component.ts
文件,放入像这样的AppComponent
类:
app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'phonecat-app',
template: `
<router-outlet></router-outlet>
<div class="view-container">
<div ng-view class="view-frame"></div>
</div>
`
})
export class AppComponent { }
它有一个很简单的模板,只包含Angular路由的<router-outlet>
和AngularJS路由的ng-view
指令。
该组件只负责渲染活动路由的内容,此外啥也不干。
该选择器告诉Angular:当应用启动时就把这个根组件插入到宿主页面的<phonecat-app>
元素中。
把这个<phonecat-app>
元素插入到index.html
中。
用它来代替AngularJS中的ng-view
指令:
index.html (body)
<body>
<phonecat-app></phonecat-app>
</body>
创建路由模块
Create the Routing Module
无论在AngularJS还是Angular或其它框架中,路由器都需要进行配置。
Angular路由器配置的详情最好去查阅下路由与导航文档。
它建议你创建一个专们用于路由器配置的NgModule
(名叫路由模块)。
app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule, UrlHandlingStrategy, UrlTree } from '@angular/router';
import { APP_BASE_HREF, HashLocationStrategy, LocationStrategy } from '@angular/common';
import { PhoneListComponent } from './phone-list/phone-list.component';
export class Ng1Ng2UrlHandlingStrategy implements UrlHandlingStrategy {
shouldProcessUrl(url: UrlTree) {
return url.toString() === '/' || url.toString() === '/phones';
}
extract(url: UrlTree) { return url; }
merge(url: UrlTree, whole: UrlTree) { return url; }
}
const routes: Routes = [
{ path: '', redirectTo: 'phones', pathMatch: 'full' },
{ path: 'phones', component: PhoneListComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ],
providers: [
{ provide: APP_BASE_HREF, useValue: '!' },
{ provide: LocationStrategy, useClass: HashLocationStrategy },
{ provide: UrlHandlingStrategy, useClass: Ng1Ng2UrlHandlingStrategy }
]
})
export class AppRoutingModule { }
该模块定义了一个routes
对象,它带有两个路由,分别指向两个电话组件,以及为空路径指定的默认路由。
它把routes
传给RouterModule.forRoot
方法,该方法会完成剩下的事。
一些额外的提供商让路由器使用“hash”策略解析URL,比如#!/phones
,而不是默认的“Push State”策略。
这里路由模块会出现一个冲突:我们还添加了一个自定义的UrlHandlingStrategy
,它会告诉Angular的路由器只处理/
和/phones
路由。
现在,修改AppModule
,让它导入这个AppRoutingModule
,并同时声明根组件AppComponent
。
这会告诉Angular,它应该使用根组件AppComponent
引导应用,并把它的视图插入到宿主页面中。
我们也可以移除app.module.ts
中对ngDoBootstrap()
的改写,因为我们正在从Angular中引导。
并且,由于PhoneListComponent
不再渲染到<phone-list>
标签下,而是路由到它,我们同样也可以去掉它的Angular选择器。
app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { Phone } from './core/phone/phone.service';
import { CheckmarkPipe } from './core/checkmark/checkmark.pipe';
import { PhoneListComponent } from './phone-list/phone-list.component';
import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
import { routeParamsProvider } from './ajs-upgraded-providers';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
FormsModule,
AppRoutingModule
],
declarations: [
AppComponent,
PhoneListComponent,
PhoneDetailComponent,
CheckmarkPipe
],
entryComponents: [
PhoneListComponent,
PhoneDetailComponent
],
providers: [
Phone,
routeParamsProvider
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
现在,我们要让AngularJS路由器只处理/phones/:phoneId
路由:
app/app.config.ts (route config)
$routeProvider
.when('/phones/:phoneId', {
template: '<phone-detail></phone-detail>'
});
为每个电话生成链接
Generate links for each phone
在电话列表中,我们不用再被迫硬编码电话详情的链接了。
我们可以通过把每个电话的id
绑定到routerLink
指令来生成它们了,该指令的构造函数会为PhoneDetailComponent
生成正确的URL:
app/phone-list/phone-list.template.html (list with links)
<ul class="phones">
<li *ngFor="let phone of getPhones()"
class="thumbnail phone-list-item">
<a [routerLink]="['/phones', phone.id]" class="thumb">
<img [src]="phone.imageUrl" [alt]="phone.name" />
</a>
<a [routerLink]="['/phones', phone.id]" class="name">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
要了解详情,请查看路由与导航页。
我们现在同时运行着两个路由器!
Angular负责处理初始URL /
,并重定向到/phones
。而AngularJS负责处理到手机详情的链接。
用这种方式,我们可以增量升级该应用,减少一次性更换路由带来的巨大风险。
下一步是迁移/phones/:phoneId
路由。
Angular路由器会传入不同的路由参数。
改正PhoneDetail
组件的构造函数,让它改用注入进来的ActivatedRoute
对象。
从ActivatedRoute.snapshot.params
中提取出phoneId
,并像以前一样获取手机的数据:
app/phone-detail/phone-detail.component.ts
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Phone, PhoneData } from '../core/phone/phone.service';
@Component({
selector: 'phone-detail',
templateUrl: './phone-detail.template.html'
})
export class PhoneDetailComponent {
phone: PhoneData;
mainImageUrl: string;
constructor(activatedRoute: ActivatedRoute, phone: Phone) {
phone.get(activatedRoute.snapshot.params['phoneId'])
.subscribe((p: PhoneData) => {
this.phone = p;
this.setImage(p.images[0]);
});
}
setImage(imageUrl: string) {
this.mainImageUrl = imageUrl;
}
}
由于这是我们要迁移的最后一个路由,因此现在可以从app/app.config.ts
中移除最后一个路由配置了,并把它添加到Angular的路由器配置里。
我们再也不需要UrlHandlingStrategy
了,因为现在Angular会处理所有路由。
app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF, HashLocationStrategy, LocationStrategy } from '@angular/common';
import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
import { PhoneListComponent } from './phone-list/phone-list.component';
const routes: Routes = [
{ path: '', redirectTo: 'phones', pathMatch: 'full' },
{ path: 'phones', component: PhoneListComponent },
{ path: 'phones/:phoneId', component: PhoneDetailComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ],
providers: [
{ provide: APP_BASE_HREF, useValue: '!' },
{ provide: LocationStrategy, useClass: HashLocationStrategy },
]
})
export class AppRoutingModule { }
我们现在运行的就是纯正的Angular应用了!
再见,AngularJS!
Say Goodbye to AngularJS
是时候把辅助训练的轮子摘下来了!让我们的应用作为一个纯粹、闪亮的Angular程序开始它的新生命吧。 剩下的所有任务就是移除代码 —— 这当然是每个程序员最喜欢的任务!
应用仍然以混合式应用的方式启动,然而这再也没有必要了。
把应用的引导(bootstrap
)方法从UpgradeAdapter
的改为Angular的。
main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
如果你还没有这么做,请从app.module.ts删除所有
UpgradeModule的引用,
以及所有用于AngularJS服务的工厂供应商(factory provider)和app/ajs-upgraded-providers.ts
文件。
还要删除所有的downgradeInjectable()
或downgradeComponent()
以及与AngularJS相关的工厂或指令声明。
因为我们不再需要降级任何组件了,也不再需要把它们列在entryComponents
中。
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CheckmarkPipe } from './core/checkmark/checkmark.pipe';
import { Phone } from './core/phone/phone.service';
import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
import { PhoneListComponent } from './phone-list/phone-list.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule
],
declarations: [
AppComponent,
PhoneListComponent,
CheckmarkPipe,
PhoneDetailComponent
],
providers: [
Phone
],
bootstrap: [ AppComponent ]
})
export class AppModule {}
我们还要完全移除了下列文件。它们是AngularJS的模块配置文件和类型定义文件,在Angular中不需要了:
app/app.module.ajs.ts
app/app.config.ts
app/core/core.module.ts
app/core/phone/phone.module.ts
app/phone-detail/phone-detail.module.ts
app/phone-list/phone-list.module.ts
还需要反安装AngularJS的外部类型定义文件。我们现在只需要Jasmine的那些。
systemjs.config.js
中的@angular/upgrade
包及其映射也可以移除了。
npm uninstall @angular/upgrade --save
npm uninstall @types/angular @types/angular-animate @types/angular-cookies @types/angular-mocks @types/angular-resource @types/angular-route @types/angular-sanitize --save-dev
最后,从index.html
和karma.conf.js
中,移除所有到AngularJS脚本的引用,比如jQuery。
当这些全部做完时,index.html
应该是这样的:
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<base href="/app/">
<title>Google Phone Gallery</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script>
System.import('/app');
</script>
</head>
<body>
<phonecat-app></phonecat-app>
</body>
</html>
这是我们最后一次看到AngularJS了!它曾经带给我们很多帮助,不过现在,是时候说再见了。
附录:升级PhoneCat的测试
Appendix: Upgrading PhoneCat Tests
测试不仅要在升级过程中被保留,它还是确保应用在升级过程中不会被破坏的一个安全指示器。 要达到这个目的,E2E测试尤其有用。
E2E测试
E2E Tests
PhoneCat项目中同时有基于Protractor的E2E测试和一些基于Karma的单元测试。 对这两者来说,E2E测试的转换要容易得多:根据定义,E2E测试通过与应用中显示的这些UI元素互动,从外部访问我们的应用来进行测试。 E2E测试实际上并不关心这些应用中各部件的内部结构。这也意味着,虽然我们已经修改了此应用程序, 但是E2E测试套件仍然应该能像以前一样全部通过。因为从用户的角度来说,我们并没有改变应用的行为。
在转成TypeScript期间,我们不用做什么就能让E2E测试正常工作。 只有当我们想做些修改而把组件及其模板升级到Angular时才需要做些处理。
需要对protractor-conf.js
做如修改,与混合应用同步:
ng12Hybrid: true
当我们开始组件和模块升级到Angular时,还需要一系列后续的修改。 这是因为E2E测试有一些匹配器是AngularJS中特有的。对于PhoneCat来说,为了让它能在Angular下工作,我们得做下列修改:
老代码 | 新代码 | 说明 |
---|---|---|
|
|
repeater匹配器依赖于AngularJS中的 |
|
|
repeater匹配器依赖于AngularJS中的 |
|
|
model匹配器依赖于AngularJS中的 |
|
|
model匹配器依赖于AngularJS中的 |
|
|
binding匹配器依赖于AngularJS的数据绑定 |
当引导方式从UpgradeModule
切换到纯Angular的时,AngularJS就从页面中完全消失了。
此时,我们需要告诉Protractor,它不用再找AngularJS应用了,而是从页面中查找Angular应用。
于是在protractor-conf.js
中做下列修改:
Replace the ng12Hybrid
previously added with the following in protractor-conf.js
:
替换之前在protractor-conf.js
中加入 ng12Hybrid
,象这样:
useAllAngular2AppRoots: true,
同样,我们的测试代码中有两个Protractor API调用内部使用了$location
。该服务没有了,
我们就得把这些调用用一个WebDriver的通用URL API代替。第一个API是“重定向(redirect)”规约:
e2e-tests/scenarios.ts
it('should redirect `index.html` to `index.html#!/phones', function() {
browser.get('index.html');
browser.waitForAngular();
browser.getCurrentUrl().then(function(url: string) {
expect(url.endsWith('/phones')).toBe(true);
});
});
然后是“电话链接(phone links)”规约:
e2e-tests/scenarios.ts
it('should render phone specific links', function() {
let query = element(by.css('input'));
query.sendKeys('nexus');
element.all(by.css('.phones li a')).first().click();
browser.getCurrentUrl().then(function(url: string) {
expect(url.endsWith('/phones/nexus-s')).toBe(true);
});
});
单元测试
Unit Tests
另一方面,对于单元测试来说,需要更多的转化工作。实际上,它们需要随着产品代码一起升级。
在转成TypeScript期间,严格来讲没有什么改动是必须的。但把单元测试代码转成TypeScript仍然是个好主意, 产品代码从TypeScript中获得的那些增益也同样适用于测试代码。
比如,在这个电话详情组件的规约中,我们不仅用到了ES2015中的箭头函数和块作用域变量这些特性,还为所用的一些 AngularJS服务提供了类型定义。
app/phone-detail/phone-detail.component.spec.ts
describe('phoneDetail', () => {
// Load the module that contains the `phoneDetail` component before each test
beforeEach(angular.mock.module('phoneDetail'));
// Test the controller
describe('PhoneDetailController', () => {
let $httpBackend: angular.IHttpBackendService;
let ctrl: any;
let xyzPhoneData = {
name: 'phone xyz',
images: ['image/url1.png', 'image/url2.png']
};
beforeEach(inject(($componentController: any,
_$httpBackend_: angular.IHttpBackendService,
$routeParams: angular.route.IRouteParamsService) => {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);
$routeParams['phoneId'] = 'xyz';
ctrl = $componentController('phoneDetail');
}));
it('should fetch the phone details', () => {
jasmine.addCustomEqualityTester(angular.equals);
expect(ctrl.phone).toEqual({});
$httpBackend.flush();
expect(ctrl.phone).toEqual(xyzPhoneData);
});
});
});
一旦我们开始了升级过程并引入了SystemJS,还需要对Karma进行配置修改。 我们需要让SystemJS加载所有的Angular新代码,
karma-test-shim.js
// /*global jasmine, __karma__, window*/
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
// Error.stackTraceLimit = Infinity; //
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
var builtPath = '/base/app/';
__karma__.loaded = function () { };
function isJsFile(path) {
return path.slice(-3) == '.js';
}
function isSpecFile(path) {
return /\.spec\.(.*\.)?js$/.test(path);
}
function isBuiltFile(path) {
return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath);
}
var allSpecFiles = Object.keys(window.__karma__.files)
.filter(isSpecFile)
.filter(isBuiltFile);
System.config({
baseURL: '/base',
// Extend usual application package list with test folder
packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
// Assume npm: is set in `paths` in systemjs.config
// Map the angular testing umd bundles
map: {
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
'@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
},
});
System.import('systemjs.config.js')
.then(importSystemJsExtras)
.then(initTestBed)
.then(initTesting);
/** Optional SystemJS configuration extras. Keep going w/o it */
function importSystemJsExtras(){
return System.import('systemjs.config.extras.js')
.catch(function(reason) {
console.log(
'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.'
);
console.log(reason);
});
}
function initTestBed(){
return Promise.all([
System.import('@angular/core/testing'),
System.import('@angular/platform-browser-dynamic/testing')
])
.then(function (providers) {
var coreTesting = providers[0];
var browserTesting = providers[1];
coreTesting.TestBed.initTestEnvironment(
browserTesting.BrowserDynamicTestingModule,
browserTesting.platformBrowserDynamicTesting());
})
}
// Import all spec files and start karma
function initTesting () {
return Promise.all(
allSpecFiles.map(function (moduleName) {
return System.import(moduleName);
})
)
.then(__karma__.start, __karma__.error);
}
这个shim文件首先加载了SystemJS的配置,然后是Angular的测试支持库,然后是应用本身的规约文件。
然后需要修改Karma配置,来让它使用本应用的根目录作为基础目录(base directory),而不是app
。
karma.conf.js
basePath: './',
一旦这些完成了,我们就能加载SystemJS和其它依赖,并切换配置文件来加载那些应用文件,而不用在Karma页面中包含它们。 我们要让这个shim文件和SystemJS去加载它们。
karma.conf.js
// System.js for module loading
'node_modules/systemjs/dist/system.src.js',
// Polyfills
'node_modules/core-js/client/shim.js',
// zone.js
'node_modules/zone.js/dist/zone.js',
'node_modules/zone.js/dist/long-stack-trace-zone.js',
'node_modules/zone.js/dist/proxy.js',
'node_modules/zone.js/dist/sync-test.js',
'node_modules/zone.js/dist/jasmine-patch.js',
'node_modules/zone.js/dist/async-test.js',
'node_modules/zone.js/dist/fake-async-test.js',
// RxJs.
{ pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false },
{ pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false },
// Angular itself and the testing library
{pattern: 'node_modules/@angular/**/*.js', included: false, watched: false},
{pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: false},
{pattern: 'systemjs.config.js', included: false, watched: false},
'karma-test-shim.js',
{pattern: 'app/**/*.module.js', included: false, watched: true},
{pattern: 'app/*!(.module|.spec).js', included: false, watched: true},
{pattern: 'app/!(bower_components)/**/*!(.module|.spec).js', included: false, watched: true},
{pattern: 'app/**/*.spec.js', included: false, watched: true},
{pattern: '**/*.html', included: false, watched: true},
由于Angular组件中的HTML模板也同样要被加载,所以我们得帮Karma一把,帮它在正确的路径下找到这些模板:
karma.conf.js
// proxied base paths for loading assets
proxies: {
// required for component assets fetched by Angular's compiler
"/phone-detail": '/base/app/phone-detail',
"/phone-list": '/base/app/phone-list'
},
如果产品代码被切换到了Angular,单元测试文件本身也需要切换过来。对勾(checkmark)管道的规约可能是最简单的,因为它没有任何依赖:
app/core/checkmark/checkmark.pipe.spec.ts
import { CheckmarkPipe } from './checkmark.pipe';
describe('CheckmarkPipe', function() {
it('should convert boolean values to unicode checkmark or cross', function () {
const checkmarkPipe = new CheckmarkPipe();
expect(checkmarkPipe.transform(true)).toBe('\u2713');
expect(checkmarkPipe.transform(false)).toBe('\u2718');
});
});
Phone
服务的测试会牵扯到一点别的。我们需要把模拟版的AngularJS $httpBackend
服务切换到模拟板的Angular Http后端。
app/core/phone/phone.service.spec.ts
import { inject, TestBed } from '@angular/core/testing';
import {
Http,
BaseRequestOptions,
ResponseOptions,
Response
} from '@angular/http';
import { MockBackend, MockConnection } from '@angular/http/testing';
import { Phone, PhoneData } from './phone.service';
describe('Phone', function() {
let phone: Phone;
let phonesData: PhoneData[] = [
{name: 'Phone X', snippet: '', images: []},
{name: 'Phone Y', snippet: '', images: []},
{name: 'Phone Z', snippet: '', images: []}
];
let mockBackend: MockBackend;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
Phone,
MockBackend,
BaseRequestOptions,
{ provide: Http,
useFactory: (backend: MockBackend, options: BaseRequestOptions) => new Http(backend, options),
deps: [MockBackend, BaseRequestOptions]
}
]
});
});
beforeEach(inject([MockBackend, Phone], (_mockBackend_: MockBackend, _phone_: Phone) => {
mockBackend = _mockBackend_;
phone = _phone_;
}));
it('should fetch the phones data from `/phones/phones.json`', (done: () => void) => {
mockBackend.connections.subscribe((conn: MockConnection) => {
conn.mockRespond(new Response(new ResponseOptions({body: JSON.stringify(phonesData)})));
});
phone.query().subscribe(result => {
expect(result).toEqual(phonesData);
done();
});
});
});
对于组件的规约,我们可以模拟出Phone
服务本身,并且让它提供电话的数据。我们可以对这些组件使用Angular的组件单元测试API。
app/phone-detail/phone-detail.component.spec.ts
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Rx';
import { async, TestBed } from '@angular/core/testing';
import { PhoneDetailComponent } from './phone-detail.component';
import { Phone, PhoneData } from '../core/phone/phone.service';
import { CheckmarkPipe } from '../core/checkmark/checkmark.pipe';
function xyzPhoneData(): PhoneData {
return {
name: 'phone xyz',
snippet: '',
images: ['image/url1.png', 'image/url2.png']
};
}
class MockPhone {
get(id: string): Observable<PhoneData> {
return Observable.of(xyzPhoneData());
}
}
class ActivatedRouteMock {
constructor(public snapshot: any) {}
}
describe('PhoneDetailComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CheckmarkPipe, PhoneDetailComponent ],
providers: [
{ provide: Phone, useClass: MockPhone },
{ provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { 'phoneId': 1 } }) }
]
})
.compileComponents();
}));
it('should fetch phone detail', () => {
const fixture = TestBed.createComponent(PhoneDetailComponent);
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain(xyzPhoneData().name);
});
});
app/phone-list/phone-list.component.spec.ts
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Rx';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SpyLocation } from '@angular/common/testing';
import { PhoneListComponent } from './phone-list.component';
import { Phone, PhoneData } from '../core/phone/phone.service';
class ActivatedRouteMock {
constructor(public snapshot: any) {}
}
class MockPhone {
query(): Observable<PhoneData[]> {
return Observable.of([
{name: 'Nexus S', snippet: '', images: []},
{name: 'Motorola DROID', snippet: '', images: []}
]);
}
}
let fixture: ComponentFixture<PhoneListComponent>;
describe('PhoneList', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PhoneListComponent ],
providers: [
{ provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { 'phoneId': 1 } }) },
{ provide: Location, useClass: SpyLocation },
{ provide: Phone, useClass: MockPhone },
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PhoneListComponent);
});
it('should create "phones" model with 2 phones fetched from xhr', () => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelectorAll('.phone-list-item').length).toBe(2);
expect(
compiled.querySelector('.phone-list-item:nth-child(1)').textContent
).toContain('Motorola DROID');
expect(
compiled.querySelector('.phone-list-item:nth-child(2)').textContent
).toContain('Nexus S');
});
xit('should set the default value of orderProp model', () => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(
compiled.querySelector('select option:last-child').selected
).toBe(true);
});
});
最后,当我们切换到Angular路由时,我们需要重新过一遍这些组件测试。对详情组件来说,我们需要提供一个Angular
RouteParams
的mock对象,而不再用AngularJS中的$routeParams
。
app/phone-detail/phone-detail.component.spec.ts
import { ActivatedRoute } from '@angular/router';
/* . . . */
class ActivatedRouteMock {
constructor(public snapshot: any) {}
}
/* . . . */
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CheckmarkPipe, PhoneDetailComponent ],
providers: [
{ provide: Phone, useClass: MockPhone },
{ provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { 'phoneId': 1 } }) }
]
})
.compileComponents();
}));
对于电话列表组件来说,我们需要为路由器本身略作设置,以便它的路由链接(routerLink
)指令能够正常工作。
app/phone-list/phone-list.component.spec.ts
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Rx';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SpyLocation } from '@angular/common/testing';
import { PhoneListComponent } from './phone-list.component';
import { Phone, PhoneData } from '../core/phone/phone.service';
/* . . . */
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PhoneListComponent ],
providers: [
{ provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { 'phoneId': 1 } }) },
{ provide: Location, useClass: SpyLocation },
{ provide: Phone, useClass: MockPhone },
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PhoneListComponent);
});