
如果你使用过一段时间的Angular,那么可能已经有了基于AngularJS的产品。Angular虽然很好,但我们总不能抛弃现有的一切,用Angular重写整个产品吧?更好的做法是对既有的AngularJS应用进行增量式升级。谢天谢地,Angular提供了一种非常棒的方式来实现它!
AngularJS和Angular的互操作性已经相当完善。在本章中,我们将讨论如何通过写混合式应用的方式来把AngularJS升级到Angular。这种混合式应用中同时运行着AngularJS和Angular框架(它们之间还可以交换数据)。
当我们讨论AngularJS和Angular之间的互操作性时,会涉及很多周边概念,下面就是其中的一些。
把AngularJS的概念映射到Angular:大体上,Angular的组件就是AngularJS的指令。它们也都用到了“服务”。不过本章是讲如何同时使用AngularJS和Angular的,所以我们假设你已经充分了解了这些基础知识。如果你还没怎么用过Angular,请先阅读第3章。
把AngularJS应用迁移到Angular的准备工作:AngularJS.5提供了新的.component方法来制作“组件型指令”。使用.component有利于为迁移到Angular作准备,另外,创建瘦控制器(或禁止使用ng-controller指令
)能把AngularJS应用重构得更好,也更容易与Angular集成。
准备AngularJS应用的另一个要点是减少或消除双向绑定,更多地使用单向数据流。也就是说,尽量不要通过修改$scope在指令之间传递数据,而是改用服务。
这些理念确实很重要,有必要进行深入探索,但本章并不会针对升级前的重构阶段讲很多类似的最佳实践。
本章要讲的是下面这一点。
写混合式AngularJS/Angular应用:Angular提供了一种方式来启动你的AngularJS应用,然后在其中写Angular的组件和服务。写完Angular组件,只要把它和AngularJS组件混在一起就可以了。另外,依赖注入体系也支持在AngularJS和Angular之间双向传递数据,因此你写的服务在AngularJS和Angular中都能运行。
它最大的好处是什么呢?因为变更检测是在Zones中运行的,所以你再也不用调用$scope.apply或担心变更检测方面的问题了。
在本章中,我们准备升级一个名叫Interest的应用,它模仿了Pinterest(如图16-1所示)。其思想在于你可以保存一枚图钉(pin),即一个带图片的链接。这些图钉会显示在列表中,而且你可以收藏(或取消收藏)一枚图钉。

图16-1 完成后的“山寨版”Pinterest
你可以到code/conversion/AngularJS和code/conversion/hybrid下载AngularJS版和混合版的完整代码。
在深入讲解之前,我们先来看看AngularJS和Angular互操作的各种使用场景。
大体来说,AngularJS的五个主要部分是:
●指令
●控制器
●作用域
●服务
●依赖注入
这些在Angular中则发生了显著的变化。你可能听说过,来自Angular核心团队的Igor与Tobias在2014
ngEurope大会上宣布他们将消灭AngularJS中的许多“核心”思想
(如图16-2所示)。具体来说,他们宣布Angular将消灭:
●$scope(以及默认的双向绑定)
●指令定义对象
●控制器
●angular.module

图16-2 在2014 ngEurope大会上,Igor和Tobias移除了AngularJS.x的很多API。摄影:Michael Bromley(已获授权)
那些使用AngularJS构建应用并习惯于AngularJS思维的人可能会问:如果移除了那些,还剩下什么?没有控制器和$scope怎么能构建Angular应用呢?
尽管有很多人喜欢夸大Angular的不同之处,但其实它仍然沿袭了AngularJS的大量核心思想。事实上,Angular用一种更简单的模型实现了同样的功能。
大体上,Angular的核心构造为:
●组件(可看作指令)
●服务
当然,还需要大量的基础设施来支撑它们的工作。比如,你需要用依赖注入体系来管理服务;需要一个强力的变更检测机制,以便在应用中更有效地传播数据变化;还需要一个高效的渲染层,以便在正确的时机渲染DOM。
那么,有了这两种不同的体系,我们需要借助哪些特性来简化互操作性呢?
●在AngularJS中使用Angular的组件:我们首先想到的是,要能写出新的Angular组件,并在AngularJS的应用中使用它们。
●在Angular中使用AngularJS的组件:我们一般不会把整个组件树完全替换成Angular的组件,而是在Angular组件之中复用那些AngularJS组件。
●服务共享:假设我们有一个UserService,想要在AngularJS和Angular之间共享它。服务通常就是一个普通的JavaScript对象,因此更抽象地说,我们需要的是一个能支持互操作的依赖注入系统。
●变更检测:如果我们在某一边进行了改动,这些变更也应该能传播到另一边。
Angular提供了所有这些场景的解决方案,本章将一一讲解。
在本章中,我们会:
●描述即将升级的AngularJS应用;
●解释如何用Angular的UpgradeAdapter来组织混合式应用;
●通过把AngularJS应用转化成混合式应用来一步步解释如何在AngularJS和Angular中共享组件(指令)与服务。
作为准备,我们先重温一下该应用的AngularJS版本。

本章假设你已经具备了关于AngularJS和ui-router
的知识。如果你对AngularJS感到吃力,请先阅读《AngularJS权威教程》
。
我们不会深入剖析和解释每个AngularJS的概念,只会回顾一下这个准备升级到Angular/混合式应用的结构。
要运行AngularJS应用,使用cd转到示例代码中的conversion/AngularJS,安装依赖,并运行该应用:
cd code/conversion/AngularJS # change directories
npm install # install dependencies
npm run go # run the app
如果没有自动打开浏览器,请手动打开URL:http://localhost:8080。
在该应用中,你可以看到用户正在收集的小玩偶。我们可以把鼠标移到某个条目上,并点击红心图标来收藏一个图钉,如图16-3所示。

图16-3 红心表示已收藏的图钉
我们还可以导航到/add页,并添加一个新的图钉。试试提交这个默认表单。
处理图片上传对于这个演示来说过于复杂了。目前,如果你想换一幅图,只要粘贴一幅图片的完整URL即可。
16.5.1 AngularJS应用的HTML
AngularJS应用中的index.html使用了一种常用的结构。
code/conversion/AngularJS/index.html
<!DOCTYPE html>
<html ng-app='interestApp'>
<head>
<meta charset="utf-8">
<title>Interest</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/sf.css">
<link rel="stylesheet" href="css/interest.css">
</head>
<body class="container-fullwidth">
<div class="page-header">
<div class="container">
<h1>Interest <small>what you're interested in</small></h1>
<div class="navLinks">
<a ui-sref='home' id="navLinkHome">Home</a>
<a ui-sref='add' id="navLinkAdd">Add</a>
</div>
</div>
</div>
<div id="content">
<div ui-view=''></div>
</div>
<script src="js/vendor/lodash.js"></script>
<script src="js/vendor/angular.js"></script>
<script src="js/vendor/angular-ui-router.js"></script>
<script src="js/app.js"></script>
</body>
</html>
●注意,我们在html标签中使用ng-app来指定该应用所用的是interestApp模块。
●我们在body的底部使用script标签来加载JavaScript脚本。
●该模板包含一个page-header指令,这里是我们的导航栏。
●我们使用了ui-router,这意味着:
●使用ui-sref来表示链接(Home和Add);
●我们希望路由器把内容放在ui-view中。
16.5.2 代码概览
我们将遍历代码中的每个部分。不过首先来简单描述一下这些活动部件。
在我们的应用中,有两个路由:
●/使用HomeController;
●/add使用AddController。
我们用一个PinsService来存放所有现有图钉的数组。HomeController渲染出图钉列表,而AddController把新的元素添加到列表中。
我们的根路由使用HomeController来渲染这些图钉,而我们用pin指令来渲染单个图钉。
PinsService用于存放应用中的数据,所以先来看看它。
16.5.3 AngularJS:PinsService
code/conversion/AngularJS/js/app.js
angular.module('interestApp', ['ui.router'])
.service('PinsService', function($http, $q){
this._pins = null;
this.pins = function(){
var self = this;
if(self._pins == null){
// initialize with sample data
return $http.get("/js/data/sample-data.json").then(
function(response){
self._pins = response.data;
return self._pins;
})
} else {
return $q.when(self._pins);
}
}
this.addPin = function(newPin){
// adding would normally be an API request so lets mock async
return $q.when(
this._pins.unshift(newPin)
);
}
})
PinsService是一个.service,它把这些图钉的数组保存在属性_.pins中。
.pins方法返回一个承诺,它会被解析(resolve)成一个图钉列表。如果_.pins为null(也就是首次访问时),我们就会从/js/data/sample-data.json中加载示例数据。
code/conversion/AngularJS/js/data/sample-data.json
[
{
"title":"sock puppets",
"description":"from:\nThe FunCraft Book of Puppets\n1976\nISBN:0-590-11936\
-2",
"user_name":"tofutti break",
"avatar_src":"images/avatars/42826303@N00.jpg",
"src":"images/pins/106033588_167d811702_o.jpg",
"url":"https://www.flickr.com/photos/tofuttibreak/106033588/",
"faved":false,
"id":"106033588"
},
{
"title":"Puppet play.",
"description":"My wife's handmade.",
"user_name":"MIKI Yoshihito(´ω)",
"avatar_src":"images/avatars/7940758@N07.jpg",
"src":"images/pins/4422575066_7d5c4c41e7_o.jpg",
"url":"https://www.flickr.com/photos/mujitra/4422575066/",
"faved":false,
"id":"4422575066"
},
{
"title":"easy to make puppets - oliver owl(detail)",
"description":"from easy to make puppets by joyce luckin(1975)",
"user_name":"gilliflower",
"avatar_src":"images/avatars/26265986@N00.jpg",
"src":"images/pins/6819859061_25d05ef2e1_o.jpg",
"url":"https://www.flickr.com/photos/gilliflower/6819859061/",
"faved":false,
"id":"6819859061"
},
.addPin方法把一个新图钉加入到图钉数组中。在这里,我们使用$q.when来返回一个承诺,就像我们真的向一台服务器发起异步调用时一样。
16.5.4 AngularJS:配置路由
我们准备用ui-router来配置这些路由。
如果你还不熟悉ui-router,请到https://github.com/angular-ui/ui-router/wiki阅读文档。
正如前面所说,我们有两个路由。
code/conversion/AngularJS/js/app.js
.config(function($stateProvider, $urlRouterProvider){
$stateProvider
.state('home', {
templateUrl:'/templates/home.html',
controller:'HomeController as ctrl',
url:'/',
resolve:{
'pins':function(PinsService){
return PinsService.pins();
}
}
})
.state('add', {
templateUrl:'/templates/add.html',
controller:'AddController as ctrl',
url:'/add',
resolve:{
'pins':function(PinsService){
return PinsService.pins();
}
}
})
$urlRouterProvider.when('', '/');
})
第一个路由/被映射到了HomeController,我们很快就会看到它的模板。注意,我们还在使用ui-router的resolve功能。这表示在为用户加载此路由之前,我们希望先调用PinsService.pins(),并且把结果(图钉列表)注入到控制器中(HomeController)。
/add路由与之类似,只是使用了另一套模板和控制器。
我们首先看看HomeController。
16.5.5 AngularJS:HomeController
HomeController很简明。我们把通过resolve注入进来的pins保存到$scope.pins中。
code/conversion/AngularJS/js/app.js
.controller('HomeController', function(pins){
this.pins = pins;
})
16.5.6 AngularJS:HomeController模板
首页的模板很小:我们用ng-repeat来循环$scope.pins中的图钉,然后用pin指令来渲染出每个图钉。
code/conversion/AngularJS/templates/home.html
<div class="container">
<div class="row">
<pin item="pin" ng-repeat="pin in ctrl.pins">
</pin>
</div>
</div>
下面深入看看这个pin指令。
16.5.7 AngularJS:pin指令
pin指令被限制(restrict)为匹配元素(E),并且具有一个template。
我们可以通过item属性把pin传进去,就像在home.html模板中所做的那样。
link函数在定义域上定义了一个名叫toggleFav的函数,它会来回切换图钉的faved属性。
code/conversion/AngularJS/js/app.js
})
.directive('pin', function(){
return {
restrict:'E',
templateUrl:'/templates/pin.html',
scope:{
'pin':"=item"
},
link:function(scope, elem, attrs){
scope.toggleFav = function(){
scope.pin.faved =!scope.pin.faved;
}
}
}
})

到2016年,该指令已经不能再作为指令最佳实践的示例了。比如,要想在AngularJS中写一个全新的指令,我应该会用AngularJS.5中新的.component函数。至少,我会用controllerAs来代替link。
但本节并不是讲解该如何写好AngularJS代码的,而是展示如何迁移现有的AngularJS代码。
16.5.8 AngularJS:pin指令模板
templates/pin.html模板在我们的页面中渲染了一个单独的图钉。
code/conversion/AngularJS/templates/pin.html
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<div class="content">
<img ng-src="{{pin.src}}" class="img-responsive">
<div class="caption">
<h3>{{pin.title}}</h3>
<p>{{pin.description | truncate:100}}</p>
</div>
<div class="attribution">
<img ng-src="{{pin.avatar_src}}" class="img-circle">
<h4>{{pin.user_name}}</h4>
</div>
</div>
<div class="overlay">
<div class="controls">
<div class="heart">
<a ng-click="toggleFav()">
<img src="/images/icons/Heart-Empty.png" ng-if="!pin.faved"></img>
<img src="/images/icons/Heart-Red.png" ng-if="pin.faved"></img>
</a>
</div>
</div>
</div>
</div>
</div>
我们在这里用到的指令都是AngularJS的内置指令:
●用ng-src来渲染img;
●接着显示pin.title和pin.description;
●用ng-if来决定是显示红心还是空心。
这里最有意思的是ng-click,它会调用toggleFav,而toggleFav会修改pin.faved属性,应用从而据此显示红心或空心(如图16-4所示)。

图16-4 红心与空心
接下来,我们看看AddController。
16.5.9 AngularJS:AddController
这个AddController比HomeController的代码要多一点。我们从定义控制器并指定要注入的服务开始。
code/conversion/AngularJS/js/app.js
.controller('AddController', function($state, PinsService, $timeout){
var ctrl = this;
ctrl.saving = false;
我们在路由器和模板中使用了controllerAs语法。这意味着我们把属性放在了this上,而不是$scope上。this的作用域在ES5 JavaScript上有点复杂,所以我们指定var ctrl = this;,以消除在嵌套的函数中引用该控制器时可能出现的歧义。
code/conversion/AngularJS/js/app.js
var makeNewPin = function(){
return {
"title":"Steampunk Cat",
"description":"A cat wearing goggles",
"user_name":"me",
"avatar_src":"images/avatars/me.jpg",
"src":"/images/pins/cat.jpg",
"url":"http://cats.com",
"faved":false,
"id":Math.floor(Math.random()* 10000).toString()
}
}
ctrl.newPin = makeNewPin();
我们创建了一个makeNewPin函数,它包含了图钉的默认构造函数和数据。
我们还通过把ctrl.newPin属性设置为该函数的调用结果初始化了该控制器。
最后,我们要定义一个函数来提交新图钉。
code/conversion/AngularJS/js/app.js
ctrl.submitPin = function(){
ctrl.saving = true;
$timeout(function(){
PinsService.addPin(ctrl.newPin).then(function(){
ctrl.newPin = makeNewPin();
ctrl.saving = false;
$state.go('home');
});
}, 2000);
}
})
本质上,该文档调用PinsService.addPin创建了一个新的图钉。不过这里还做了一些别的事情。
在真实的应用中,这类操作几乎总会向服务器发起一次调用。这里我们使用$timeout来模拟此效果。(实际上,你也可以移除$timeout函数,程序仍然能正常工作。在这里调用它是为了延缓程序的响应速度,让我们有机会看见Saving……提示。)
我们要给用户一些提示,好让他们知道我们正在保存图钉,因此设置ctrl.saving = true。
我们调用PinsService.addPin,并把ctrl.newPin传给它。addPin会返回一个承诺,我们在这个承诺的回调函数中:
(1)把ctrl.newPin恢复成原始值;
(2)把ctrl.saving设置为false,因为已经保存好了图钉;
(3)使用$state服务把用户重定向到首页去,在那里可以看到新的图钉。
下面是AddController的完整代码。
code/conversion/AngularJS/js/app.js
.controller('AddController', function($state, PinsService, $timeout){
var ctrl = this;
ctrl.saving = false;
var makeNewPin = function(){
return {
"title":"Steampunk Cat",
"description":"A cat wearing goggles",
"user_name":"me",
"avatar_src":"images/avatars/me.jpg",
"src":"/images/pins/cat.jpg",
"url":"http://cats.com",
"faved":false,
"id":Math.floor(Math.random()* 10000).toString()
}
}
ctrl.newPin = makeNewPin();
ctrl.submitPin = function(){
ctrl.saving = true;
$timeout(function(){
PinsService.addPin(ctrl.newPin).then(function(){
ctrl.newPin = makeNewPin();
ctrl.saving = false;
$state.go('home');
});
}, 2000);
}
})
16.5.10 AngularJS:AddController模板
/add路由会渲染add.html模板。

图16-5 新增图钉的表单
该模板使用ng-model来把input标签绑定到控制器上的newPin属性。
这里值得关注的是:
●我们在提交按钮上使用ng-click来调用ctrl.submitPin;
●如果ctrl.saving为真,那么就要显示一条Saving……消息。
code/conversion/AngularJS/templates/add.html
<div class="container">
<div class="row">
<form class="form-horizontal">
<div class="form-group">
<label for="title"
class="col-sm-2 control-label">Title</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="title"
placeholder="Title"
ng-model="ctrl.newPin.title">
</div>
</div>
<div class="form-group">
<label for="description"
class="col-sm-2 control-label">Description</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="description"
placeholder="Description"
ng-model="ctrl.newPin.description">
</div>
</div>
<div class="form-group">
<label for="url"
class="col-sm-2 control-label">Link URL</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="url"
placeholder="Link URL"
ng-model="ctrl.newPin.url">
</div>
</div>
<div class="form-group">
<label for="url"
class="col-sm-2 control-label">Image URL</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="url"
placeholder="Image URL"
ng-model="ctrl.newPin.src">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit"
class="btn btn-default"
ng-click="ctrl.submitPin()">Submit</button>
</div>
</div>
<div ng-if="ctrl.saving">
Saving……
</div>
</form>
</div>
</div>
16.5.11 AngularJS:总结
我们终于有了要升级的AngularJS应用。该应用的复杂度正好能让我们演示如何向Angular迁移。
现在,我们已经为往现有的AngularJS应用中引入一些Angular的技术作好了准备。
开始在浏览器中使用Angular之前,我们需要对应用的结构进行一些调整。
你可以在code/conversion/hybrid找到这些示例代码。
16.6.1 混合式应用的结构
创建混合式应用的第一步是确保你同时加载了AngularJS和Angular的依赖。不过每个人遇到的具体情况可能会略有不同。
在这个例子中,我们已经提供了AngularJS的库(在js/vendor中)。接下来还要从npm中加载Angular的库。
在你的项目中,可能需要同时提供这两个库,比如使用Bower
等。不过对于Angular来说,用npm更省事,而且我们也建议使用npm来安装Angular。
●用package.json指定依赖
你可以通过npm来根据文件package.json安装依赖。下面是这个混合式应用例子中的package.json。
code/conversion/hybrid/package.json
{
"name":"ng-hybrid-pinterest",
"version":"0.0.1",
"description":"toy pinterest clone in AngularJS/Angular hybrid",
"contributors":[
"Nate Murray <nate@fullstack.io>",
"Felipe Coury <felipe@ng-book.com>"
],
"main":"index.js",
"private":true,
"scripts":{
"clean":"rm -f ts/*.js ts/*.js.map ts/components/*.js ts/components/*.js.ma\
p ts/services/*.js ts/services.js.map",
"tsc":"./node_modules/.bin/tsc",
"tsc:w":"./node_modules/.bin/tsc -w",
"serve":"./node_modules/.bin/live-server ——host=localhost ——port=8080 .",
"e2e:serve":"npm run tsc && ./node_modules/.bin/live-server ——host=localhos\
t ——port=8080 ——no-browser .",
"go":"concurrent \"npm run tsc:w\" \"npm run serve\" "
},
"dependencies":{
"@angular/common":"2.4.1",
"@angular/compiler":"2.4.1",
"@angular/core":"2.4.1",
"@angular/forms":"2.4.1",
"@angular/http":"2.4.1",
"@angular/platform-browser":"2.4.1",
"@angular/platform-browser-dynamic":"2.4.1",
"@angular/router":"3.4.1",
"@angular/upgrade":"2.0.0-rc.6",
"@types/jasmine":"2.5.40",
"core-js":"2.4.1",
"es6-shim":"0.35.0",
"reflect-metadata":"0.1.9",
"rxjs":"5.0.2",
"systemjs":"0.19.6",
"ts-helpers":"1.1.1",
"tslint":"3.7.0-dev.2",
"typings":"0.8.1",
"zone.js":"0.7.4"
},
"devDependencies":{
"@types/jasmine":"2.2.30",
"@types/node":"6.0.42",
"concurrently":"1.0.0",
"jasmine-spec-reporter":"2.5.0",
"karma":"0.12.22",
"karma-chrome-launcher":"0.1.4",
"karma-jasmine":"0.1.5",
"live-server":"0.9.0",
"protractor":"4.0.14",
"ts-node":"1.2.1",
"typescript":"2.0.3"
}
}
如果你不熟悉其中的某个包,最好自己去发现它的用途。比如rxjs是一个为我们提供可观察对象的库,而systemjs提供的是模块加载器,我们将在本章中用到它。
一旦添加了Angular的依赖,就可以运行npm install命令来安装它们了。
●编译代码
你可能注意到了,package.json中的"script"属性中包含另一个属性"tsc"。这表示当我们运行命令npm run tsc时,它就会调用TypeScript编译器来编译我们的代码。
我们准备在这个例子中使用TypeScript,同时AngularJS的代码仍然使用JavaScript。
要这么做,就要先把所有TypeScript代码放进ts/文件夹里,把所有JavaScript代码放进js/文件夹里。
我们用tsconfig.json文件来配置TypeScript编译器。关于此文件,现在你只要知道一点就可以了:filesGlob属性指定了适配规则"./ts/**/*.ts"。它的意思是“当运行TypeScript编译器时,我们希望编译ts/目录下所有以.ts结尾的文件”。
在该项目中,浏览器只会加载JavaScript。因此我们要使用TypeScript编译器(tsc)来把这些代码编译成JavaScript,然后再把AngularJS和Angular的JavaScript代码加载进浏览器中。
●加载index.html依赖
现在,我们已经设置好了依赖和编译器,接着就要把这些JavaScript文件加载到浏览器中了。因此,我们添加script标签。
code/conversion/AngularJS/hybrid/index.html
<div id="content">
<div ui-view=''></div>
</div>
<!—— Libraries ——>
<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/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="js/vendor/angular.js"></script>
<script src="js/vendor/angular-ui-router.js"></script>
我们从node_modules/中加载的文件是Angular及其依赖,而从js/vendor/中加载的文件则是AngularJS及其依赖。
但是你可能已经注意到了,我们还没有在HTML标签中加载任何自己的代码。要加载这些代码,就要使用System.js。
●配置System.js
在这个例子中,我们准备把System.js用作模块加载器。
我们还可以使用webpack(就像在本书其他例子中所用的那样)或很多其他的加载器(比如requirejs等)。不过,System.js是一个很不错并且具有可伸缩性的加载器,常常和Angular一起使用。本章会提供一个漂亮的示例,向你展示如何通过System.js使用Angular。
要配置System.js,需要在index.html的<script>标签中进行如下修改:
<script src="resources/systemjs.config.js"></script>
System.import('ts/app.js')
.then(null, console.error.bind(console));
System.import('ts/app.js')说明该应用的入口点是ts/app.js文件。当我们写混合式Angular应用时,Angular的代码会成为入口点。这很容易理解,因为Angular提供了对AngularJS的向后兼容能力。我们很快就会看到如何引导该应用。
这里要注意的另一个问题是,我们正在ts/目录下加载.js文件。为什么呢?这是因为TypeScript编译器会在页面加载时把这些文件编译成JavaScript。
我们已经在resources/systemjs.config.js中配置好了System.js。此文件中包含了几乎标准化的配置方式,但现在我们要把AngularJS应用加载到Angular代码中,那就不得不添加一个特殊的属性interestAppNg1了,它指向我们的AngularJS应用。该选项让我们能在TypeScript代码中这样用:
import 'interestAppNg1'; // "bare import" for side-effects
当模块加载器看到字符串'interestAppNg1'时,就会去./js/app.js中加载我们的AngularJS应用。
packages属性指出ts包(package)中的文件将会具有.js扩展名,并使用System.js来注册(register)这种模块格式。
TypeScript编译器可以输出多种模块格式。System.js的format需要与编译器输出的模块格式保持一致。这里register的模块格式之所以能直接使用,是因为我们在tsconfig.json中把compilerOptions.module指定成了"system"格式。

要配置好System.js是很难的,有大量潜在选项。
这不是一本关于模块加载器的书,事实上,只是深入讲解如何配置System.js和其他JavaScript模块加载器就足够写一整本书了。
目前,我们不准备深入讨论模块加载器,不过如果你想了解更多,请参阅https://github.com/systemjs/systemjs/blob/master/docs/config-api.md。
你想阅读关于JavaScript模块加载器的书吗?我们正在考虑写一本。如果你想及时收到通知,请在这里留下你的邮箱:http://eepurl.com/bMOaEX。
16.6.2 引导混合式应用
现在项目结构已经就绪,我们来启动这个应用吧。
还记得吗?在AngularJS中,有两种方式可以启动应用:
(1)使用ng-app指令,比如在HTML中写ng-app='interestApp';
(2)在JavaScript中使用angular.bootstrap。
在混合式应用中,我们要使用来自UpgradeAdapter的新引导方法。
我们还要改为从代码中启动应用,因此请确保从index.html中移除了ng-app指令。
一个最简的启动代码是这样的:
// code/conversion/hybrid/ts/app.ts
import {
NgModule,
forwardRef
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeAdapter } from '@angular/upgrade';
declare var angular:any;
import 'interestAppNg1'; // "bare import" for side-effects
/*
* Create our upgradeAdapter
*/
const upgradeAdapter:UpgradeAdapter = new UpgradeAdapter(
forwardRef(()=> MyAppModule)); // <—— notice forward reference
// ……
// upgrade and downgrade components in here
// ……
/*
* Create our app's entry NgModule
*/
@NgModule({
declarations:[ MyNg2Component, …… ],
imports:[
CommonModule,
BrowserModule
],
providers:[ MyNg2Services, …… ]
})
class MyAppModule { }
/*
* Bootstrap the App
*/
upgradeAdapter.bootstrap(document.body, ['interestApp']);
我们先导入了UpgradeAdapter,然后创建它的实例upgradeAdapter。
不过,UpgradeAdapter的构造函数需要一个NgModule,它用于启动我们的Angular应用,但我们还没有定义它呢!要解决这个问题,就用forwardRef函数来取得NgModule的“前向引用”(后面会声明它)。
当我们定义自己的NgModule也就是MyAppModule时(具体到这个应用中,它应该是InterestAppModule),写法和定义其他Angular的NgModule没有区别:我们放进了声明(declarations)、导入(imports)和提供者(providers)等。
最后,我们告诉upgradeAdapter在document.body元素上bootstrap此应用,并指定了AngularJS应用的模块名。
这将会在启动Angular应用的同时启动AngularJS应用!接下来,我们就开始一点一点地用Angular替换掉它。
16.6.3 我们要升级什么
先来讨论一下这个例子中的哪些部分需要迁移到Angular,哪些仍然留在AngularJS。
●首页
需要注意的第一点是,我们仍将使用AngularJS来管理路由。当然,Angular有自己的路由,你可以在第7章中读到它。但是如果你正在构建一个混合式应用,很可能已经用AngularJS配置过很多路由了。因此,在这个例子中,我们仍然沿用ui-router作为路由体系。
在首页中,我们准备把Angular的组件嵌套在AngularJS的指令中。在这个例子中,就是把“图钉控件”转变成Angular的组件(如图16-6所示)。也就是说,我们的pin指令将调用Angular的pin-controls组件,而pin-controls组件负责渲染出用来表示收藏的心型图标。

图16-6 首页的AngularJS和Angular组件
尽管这是一个很小的例子,但它展示了一种强有力的想法:如何在ng的不同版本之间无缝地交换数据。
●About页
我们也会在About页上使用AngularJS来实现路由和页眉。不过,在About页上,我们将把整个表单替换成Angular的组件:AddPinComponent(如图16-7所示)。

图16-7 About页的AngularJS和Angular组件
回想一下,该表单会往PinsService上添加一个新的图钉。在这个例子中,我们需要通过某种方式来让Angular的AddPinComponent访问到AngularJS的PinsService。
另外,在添加新的图钉之后,该应用应该自动导航到首页。不过,要想改变当前路由,我们需要在Angular的AddPinComponent中使用来自AngularJS中ui-router库的$state服务。因此,我们同样需要确保$state服务也能在AddPinComponent中使用。
●服务
我们刚才说过,有两个AngularJS的服务将会升级到Angular:
●PinsService
●$state
不过我们也想看看如何把一个Angular服务降级,以供AngularJS使用。为此,我们稍后会用TypeScript/Angular来创建一个AnalyticsService服务,并把它共享给AngularJS。
●盘点
概括起来,我们准备讲解下列内容:
●把Angular的PinControlsComponent降级到AngularJS(用来实现收藏按钮);
●把Angular的AddPinComponent降级到AngularJS(用来实现新增图钉页面);
●把Angular的AnalyticsService降级到AngularJS(用来进行事件记录);
●把AngularJS的PinsService升级到Angular(用来新增图钉);
●把AngularJS的$state服务升级到Angular(用来控制路由)。
16.6.4 插一小段内容:类型文件
TypeScript最美妙的一点就是编译时类型检查。不过,如果你正在构建一个混合式应用,那么估计你打算集成到项目中的JavaScript代码大部分是无类型的。
当你试图在TypeScript中使用JavaScript代码时,可能会收到编译器错误,因为编译器不知道你的JavaScript对象结构如何。你可以尝试把它们全部转换成<any>,但这样不但看起很来丑而且容易出错。
更好的方案是给TypeScript编译器提供自定义类型注解。然后,编译器就能用这些类型信息来强化你的JavaScript代码了。
比如,还记得我们是怎样在AngularJS版本的makeNewPin中创建图钉对象的吗?
code/conversion/AngularJS/js/app.js
var makeNewPin = function(){
return {
"title":"Steampunk Cat",
"description":"A cat wearing goggles",
"user_name":"me",
"avatar_src":"images/avatars/me.jpg",
"src":"/images/pins/cat.jpg",
"url":"http://cats.com",
"faved":false,
"id":Math.floor(Math.random()* 10000).toString()
}
}
ctrl.newPin = makeNewPin();
如果能把这些对象的结构告诉编译器该多好!那样就不用到处求助于any了。
此外,我们准备在Angular/TypeScript中使用ui-router中的$state服务,同样要把这个服务中有哪些可用的函数告诉编译器。
因此,虽然为TypeScript提供自定义类型信息是TypeScript的分内之事(与Angular无关),但我们还是得亲力亲为。现在之所以还缺少这么多类型定义文件,是因为TypeScript才发布没多久,仍然相对较新。
在本节中,我会告诉你如何为TypeScript制作自定义类型文件(custom typing)。
如果你已经很熟悉如何创建和使用TypeScript的类型定义文件,请放心大胆地跳过本节。
●类型文件
在TypeScript中,可以通过书写类型定义文件(typing definition file)来描述我们的代码结构。类型定义文件通常以扩展名.d.ts结尾。
当我们写TypeScript代码时,通常不用写.d.ts文件,因为TypeScript文件本身已经包含了类型信息。只有当要为某些外来的JavaScript代码添加类型信息时,才需要写.d.ts文件。
例如,为了描述我们的图钉对象,可以为它写一个interface。
code/conversion/hybrid/js/app.d.ts
export interface Pin {
title:string;
description:string;
user_name:string;
avatar_src:string;
src:string;
url:string;
faved:boolean;
id:string;
}
注意,我们不是在声明一个类,也没有创建实例,而是定义了接口的形态(类型)。
要使用.d.ts文件,需要告诉TypeScript它们在哪里。最简单的方式就是修改tsconfig.json文件。比如,假设有一个名为js/app.d.ts的文件,我们就可以像这样添加它:
// tsconfig.json
"compilerOptions":{ …… },
"files":[
"ts/app.ts",
"js/app.d.ts"
],
// more……
仔细看这里的文件路径。我们要从ts/app.ts中加载TypeScript,从js/目录下加载app.d.ts文件。这是因为js/app.d.ts文件是为js/app.js(这是AngularJS的JavaScript文件,而不是Angular的TypeScript文件)准备的类型文件。
我们这就一点点把app.d.ts写出来。首先来看一个现有工具typings,以帮助我们使用第三方TypeScript定义文件。
●使用typings管理第三方库
typings是一个用来为第三方库管理TypeScript类型定义文件的工具。
我们准备使用angular-ui-router,所以要用typings来安装angular-ui-router的类型信息。下面是操作步骤。
先安装好typings,可以用命令npm install -g typings来安装。
接下来,配置一个typings.json文件,可以用命令typings init来创建(或者使用现成的)。
然后,我们通过命令typings install angular-ui-router ——save来安装所需的包。
注意,typings命令创建了一个typings目录,其中包含文件browser.d.ts。这个browser.d.ts文件是所有被typings管理的类型定义文件的总入口点。也就是说,如果你写了自己的类型定义文件,那么它们不会被包含在这里,但通过typings工具安装的类型定义都会被加载到此文件的reference标签下。
不要直接修改typings/browser.d.ts文件!typings会替你管理这个文件。如果你修改了它,那么这些修改就会被覆盖。
现在,我们有了类型定义文件typings/browser.d.ts,该如何使用它呢?我们得先把它告诉编译器才行。可以通过tsconfig.json来做到这一点:
// tsconfig.json
"compilerOptions":{ …… },
"files":[
"typings/browser.d.ts",
"ts/app.ts",
"js/app.d.ts"
],
// more……
注意,我们把typings/browser.d.ts文件添加到了files数组中。这会告诉编译器我们要在编译时包含typings下的类型信息。

假如我们要加载另一个库(比如underscore),而且同样希望用System.js加载它,该怎么办呢?
整体思路是,你要:(1)让类型信息在编译时可用;(2)让代码在运行时可用。
具体办法如下。
(1)typings install underscore:安装类型信息文件。
(2)npm install underscore:在node_modules中安装JavaScript文件。
(3)在index.html中调用System.config的地方往paths下增加一句underscore:'./node_modules/underscore/underscore.js'。
(4)然后在TypeScript中通过import * as _ from 'underscore';导入下划线。
(5)最后使用下划线,就像这样:let foo = _.map([1,2,3],(x)=> x + 1);。

我们已经在这个应用中做完了typings install,所以你不必自己安装这些依赖了。
实际上,如果你运行typings install,将会收到一个错误:
node_modules/angular2/typings/angular-protractor/angular-protractor.d.ts(1679,13\):error TS2403:Subsequent variable declarations must have the same type. Vari\able '$' must be of type 'JQueryStatic', but here has type 'cssSelectorHelper'.
这个bug是因为jquery和angular的类型信息都试图把一个类型赋给$变量。在本书出版时,临时性的解决方案是打开typings/jquery/jquery.d.ts文件,并注释掉这一行:
// declare var $:JQueryStatic; // - ng-book told me to comment this
当然,如果你想在TypeScript中通过$来访问jQuery特有的类型信息,就会出错(不过本例中不存在这种情况)。
●自定义类型文件
能使用现成的第三方类型定义文件固然好,不过还有一些场景是找不到现有类型定义文件的,特别是我们自己写的代码。
通常,当我们写自定义类型信息文件时,会把它和相应的JavaScript代码放在一起,因此我们来创建一个js/app.d.ts文件。
code/conversion/hybrid/js/app.d.ts
declare module interestAppNg1 {
export interface Pin {
title:string;
description:string;
user_name:string;
avatar_src:string;
src:string;
url:string;
faved:boolean;
id:string;
}
export interface PinsService {
pins():Promise<Pin[]>;
addPin(pin:Pin):Promise<any>;
}
}
declare module 'interestAppNg1' {
export = interestAppNg1;
}
我们用declare关键字来制作“周边声明”(ambient declaration),意思是我们定义了一个并非来自于TypeScript文件的变量。在这个例子中,我们声明了两个接口:
(1)Pin
(2)PinsService
Pin接口用来描述图钉对象的属性名及其值类型。
PinsService接口则用来描述这个PinsService中两个方法的类型。
●pins()返回一个由Pin数组构成的Promise;
●addPin()接收一个Pin参数,并返回一个Promise。

学习写类型定义文件的更多知识
如果要学习关于写.d.ts文件的更多知识,下列链接会很有帮助:
●“TypeScript手册:与其他JavaScript库协同工作”(http://www.types-criptlang.org/Handbook#modules-working-with-other-javascript-libraries)
●“TypeScript手册:书写类型定义文件”(https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Writing%20Definition%20Files.md)
●“快速提示:TypeScript的declare关键字”(http://blogs.microsoft.co.il/gilf/2013/07/22/quick-tip-typescript-declare-keyword/)
你可能已经注意到了,我们并没有在AngularJS的JavaScript代码的任何地方声明令牌interestAppNg1。这是因为interestAppNg1只是我们用来在TypeScript代码中引用这些JavaScript代码时所用的标识符,并不是类型的一部分。
我们已经完成了这个文件,可以导入这些类型了,就像这样:
import { Pin, PinsService } from 'interestAppNg1';
16.6.5 写Angular的PinControlsComponent
我们刚刚明白了类型信息,那就言归正传,继续看混合式应用吧。
我们首先要做的是写一个Angular版的PinControlsComponent,这样才能把Angular的组件嵌入到AngularJS的指令中。PinControlsComponent为收藏的图钉显示心型图标,点击它就可以来回切换状态。
先从导入Pin类型开始,然后定义另一些需要的常量。
code/conversion/hybrid/ts/components/PinControlsComponent.ts
/*
* PinControls:a component that holds the controls for a particular pin
*/
import {
Component,
Input,
Output,
EventEmitter
} from '@angular/core';
import { NgIf } from '@angular/common';
import { Pin } from 'interestAppNg1';
接下来写@Component注解。
code/conversion/hybrid/ts/components/PinControlsComponent.ts
@Component({
selector:'pin-controls',
template:`
<div class="controls">
<div class="heart">
<a(click)="toggleFav()">
<img src="/images/icons/Heart-Empty.png" *ngIf="!pin.faved" />
<img src="/images/icons/Heart-Red.png" *ngIf="pin.faved" />
</a>
</div>
</div>
`
})
注意,这里匹配的是pin-controls元素。
我们的模板和AngularJS版本的很像,只是把(click)和*ngIf改成了用Angular的模板语法。
现在的组件定义类变成了下面这样。
code/conversion/hybrid/ts/components/PinControlsComponent.ts
export class PinControlsComponent {
@Input()pin:Pin;
@Output()faved:EventEmitter<Pin> = new EventEmitter<Pin>();
toggleFav():void {
this.faved.next(this.pin);
}
}
注意,我们并没有在@Component注解中指定inputs和outputs,而是直接在类的属性上使用了@Input和@Output注解。用这种方式为属性提供类型信息更加简便。
该组件将接收一个pin参数作为输入,也就是我们管理的Pin对象。
该组件指定了一个名叫faved的输出参数。这跟我们在AngularJS应用中的用法略有不同。如果你查看toggleFav的实现,会发现我们所做的是通过EventEmitter把当前图钉发给了外界。
这是因为我们已经在AngularJS中实现了更改faved状态的方法,所以不希望在Angular中重新实现一模一样的功能(但你也可能希望再次实现,这取决于你们开发组内的约定)。
16.6.6 使用Angular的PinControlsComponent
有了Angular的pin-controlls组件,我们就可以在模板中使用它了。现在的pin.html模板变成了下面这样。
code/conversion/hybrid/templates/pin.html
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<div class="content">
<img ng-src="{{pin.src}}" class="img-responsive">
<div class="caption">
<h3>{{pin.title}}</h3>
<p>{{pin.description | truncate:100}}</p>
</div>
<div class="attribution">
<img ng-src="{{pin.avatar_src}}" class="img-circle">
<h4>{{pin.user_name}}</h4>
</div>
</div>
<div class="overlay">
<pin-controls [pin]="pin"
(faved)="toggleFav($event)"></pin-controls>
</div>
</div>
</div>
该模板是属于AngularJS指令的,因此我们可以在里面使用AngularJS的指令,比如ng-src。不过,要注意使用Angular中pin-controls组件的那一行:
<pin-controls [pin]="pin"
(faved)="toggleFav($event)"></pin-controls>
有意思的是,我们在同时使用Angular输入属性的方括号语法[pin]以及Angular输出属性的圆括号语法(faved)。
在混合式应用中,当你在AngularJS中使用Angular指令时,仍然可以照常使用Angular的语法。
通过输入属性[pin],可以把来自AngularJS指令scope上的pin属性传进去。
在输出参数(faved)中,我们调用了AngularJS指令scope上的toggleFav函数。注意看这里的实现方式:我们没有在Angular指令中修改pin.faved状态(虽然我们也能这么做);反之,我们只是让Angular的PinControlsComponent在调用toggleFav的时候把这个pin发给外界。(如果没看明白,请再回头看看PinControlsComponent的toggleFav。)
我们这么做是为了告诉你:可以保持AngularJS中的现有功能(scope.toggleFav)不变,只把组件迁移到Angular。在这个例子中,AngularJS的pin指令监听了Angular PinControlsComponent上的faved事件。
如果你刷新这个页面,可能会注意到它无法正常工作,那是因为我们还缺少一个很重要的步骤:把PinControlsComponent降级到AngularJS。
16.6.7 把Angular的PinControlsComponent降级到AngularJS
要想让我们的组件跨越Angular和AngularJS的界线,最后一步是使用upgradeAdapter来降级这些组件(或者升级,我们稍后会看到)。
我们在app.ts文件中执行这些降级工作(也就是调用upgradeAdapter.bootstrap的地方)。
首先,我们需要导入必备的angular库。
code/conversion/hybrid/ts/app.ts
import {
NgModule,
forwardRef
} from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormsModule,
} from '@angular/forms';
import { BrowserModule } from "@angular/platform-browser";
import { UpgradeAdapter } from '@angular/upgrade';
declare var angular:any;
import 'interestAppNg1'; // "bare import" for side-effects
然后,我们用(几乎)标准的AngularJS方式来创建一个.directive。
code/conversion/hybrid/ts/app.ts
angular.module('interestApp')
.directive('pinControls',
upgradeAdapter.downgradeNg2Component(PinControlsComponent))
记住我们已经导入了'interestAppNg1',它会加载我们的AngularJS应用,而AngularJS应用中调用了angular.module('interestApp', [])。也就是说,我们的AngularJS应用已经通过angular注册好了interestApp模块。
现在,我们要通过调用angular.module('interestApp')来找到该模块,然后把指令添加到其中,就像我们在AngularJS中的标准做法那样。

angular.module的获取(getter)和设置(setter)语法
还记得吗?当往angular.module函数的第二个参数中传入一个数组时,我们就是在创建模块。比如angular.module('foo', [])将创建一个名叫foo的模块。我们非正式地将其称为设置语法。
同样,如果我们省略了这个数组,就是在获取一个模块(假设它已经存在)。比如angular.module('foo')将获取foo模块。我们称其为获取语法。
在这个例子中,如果我们忘了这项限制,并且在app.ts(Angular)中调用angular.module('interestApp',
[]),就会意外地覆盖现有的interestApp模块。你的应用将无法正常工作。千万要小心!
我们调用.directive并创建了一个名叫'pinControls'的指令。这是一种标准的AngularJS实践。它的第二个参数是指令定义对象(directive definition object,DDO),我们不会手动创建DDO,而是调用upgradeAdapter.downgradeNg2Component。
downgradeNg2Component会把我们的PinControlsComponent转换成与AngularJS兼容的指令。干净!漂亮!
刷新一下,你会发现收藏功能仍然正常工作(如图16-8所示),但我们已经把目前的实现方式改成在AngularJS中嵌入Angular了!

图16-8 收藏功能仍然很棒
16.6.8 用Angular添加图钉
接下来要用Angular组件对添加图钉的页面进行升级(如图16-9所示)。

图16-9 新增图钉的表单
回想一下,这个页面一共做了三件事:
(1)为用户提供一个用来描述这个图钉的表单;
(2)借助PinsService把新的图钉添加到图钉列表中;
(3)把用户重定向到首页。
我们来看看该如何在Angular中做到这些。
Angular提供了一个强力的表单库,所以这没什么难度。那我们就来写一个正统的Angular表单吧。
不过,PinsService仍然来自AngularJS。通常,我们会有很多来自AngularJS的既有服务,但又没那么多时间把它们都改写成Angular的。因此在这个例子中,我们仍然把PinsService保留为AngularJS对象,并把它注入到Angular中。
与之类似,我们把来自AngularJS的ui-router作为路由系统。要想在ui-router中进行页面跳转,就得使用$state服务,它是一个AngularJS服务。
那么,在这里要做的就是把PinsService和$state服务从AngularJS升级到Angular,这已经是最简易的方式了。
16.6.9 把AngularJS的PinsService和$state升级到Angular
要想升级AngularJS的服务,我们可以调用upgradeAdapter.upgradeNg1Provider。
code/conversion/hybrid/ts/app.ts
/*
* Expose our AngularJS content to Angular
*/
upgradeAdapter.upgradeNg1Provider('PinsService');
upgradeAdapter.upgradeNg1Provider('$state');
这样就足够了。现在我们可以把AngularJS的服务注入(@Inject)到Angular的组件中,就像这样:
class AddPinComponent {
constructor(@Inject('PinsService')public pinsService:PinsService,
@Inject('$state')public uiState:IStateService){
}
// ……
// now you can use this.pinsService
// or this.uiState
// ……
}
在这个构造函数中,有几点需要注意。
@Inject注解的意思是,我们要把参数中指定的可注入对象解析出来,赋值给紧随其后的变量。比如这里的pinsService将被赋值为我们在AngularJS中定义的服务PinsService。
在TypeScript语法中,在constructor中使用public关键字其实是一种简写形式,用来把该变量赋值给this。也就是说,当我们写public pinsService时,其实是在做两件事:(1)在该类上定义一个实例属性pinsService;(2)把构造函数的参数pinsService赋值给this.pinsService。
最终的效果是我们可以在这个类中访问this.pinsService了。
最后,我们定义了所注入的两个服务的类型:PinsService和IStateService。
PinsService来自我们以前定义过的app.d.ts。
code/conversion/hybrid/js/app.d.ts
export interface PinsService {
pins():Promise<Pin[]>;
addPin(pin:Pin):Promise<any>;
}
IStateService来自ui-router的类型文件,它是我们以前用typings工具安装的。
通过把这些服务的类型信息告诉TypeScript,我们在写代码时就可以享受类型检查带来的好处了。
下面来写完AddPinComponent的剩余部分。
16.6.10 写Angular版的AddPinComponent
我们先从导入所需的类型信息开始。
code/conversion/hybrid/ts/components/AddPinComponent.ts
/*
* AddPinComponent:a component that controls the "add pin" page
*/
import {
Component,
Inject
} from '@angular/core';
import { Pin, PinsService } from 'interestAppNg1';
import { IStateService } from 'angular-ui-router';
注意,我们导入了自定义类型Pin和PinsService,还从angular-ui-router中导入了IStateService。
●AddPinComponent的@Component
这个@Component注解非常简明。
code/conversion/hybrid/ts/components/AddPinComponent.ts
@Component({
selector:'add-pin',
templateUrl:'/templates/add-Angular.html'
})
●AddPinComponent模板
我们使用templateUrl来加载模板。在该模板中,我们的表单和AngularJS中的表单非常像,但所用的是Angular的表单指令集。
我们在这里不准备深入讲解ngModel/ngSubmit。如果你想深入了解Angular表单的工作原理,请阅读第5章,我们在那里对表单进行了详细讲解。
code/conversion/hybrid/templates/add-Angular.html
<div class="container">
<div class="row">
<form(ngSubmit)="onSubmit()"
class="form-horizontal">
<div class="form-group">
<label for="title"
class="col-sm-2 control-label">Title</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="title"
name="title"
placeholder="Title"
[(ngModel)]="newPin.title">
</div>
这里使用了两个指令:ngSubmit和ngModel。
我们在表单上使用了(ngSubmit)。这样当表单被提交时,就会调用onSubmit函数。(我们会在稍后的AddPinComponent控制器中定义onSubmit函数。)
我们使用[(ngModel)]来把title输入框的值绑定到控制器中newPin.title的值。
下面是完整的模板代码。
code/conversion/hybrid/templates/add-Angular.html
<div class="container">
<div class="row">
<form(ngSubmit)="onSubmit()"
class="form-horizontal">
<div class="form-group">
<label for="title"
class="col-sm-2 control-label">Title</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="title"
name="title"
placeholder="Title"
[(ngModel)]="newPin.title">
</div>
</div>
<div class="form-group">
<label for="description"
class="col-sm-2 control-label">Description</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="description"
name="description"
placeholder="Description"
[(ngModel)]="newPin.description">
</div>
</div>
<div class="form-group">
<label for="url"
class="col-sm-2 control-label">Link URL</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="url"
name="url"
placeholder="Link URL"
[(ngModel)]="newPin.url">
</div>
</div>
<div class="form-group">
<label for="url"
class="col-sm-2 control-label">Image URL</label>
<div class="col-sm-10">
<input type="text"
class="form-control"
id="url"
name="url"
placeholder="Image URL"
[(ngModel)]="newPin.src">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit"
class="btn btn-default"
>Submit</button>
</div>
</div>
<div *ngIf="saving">
Saving……
</div>
</form>
●AddPinComponent控制器
现在我们就可以定义AddPinComponent了。先从两个实例变量开始。
code/conversion/hybrid/ts/components/AddPinComponent.ts
export class AddPinComponent {
saving:boolean = false;
newPin:Pin;
saving会告诉用户我们正在进行保存,而newPin用于存储我们正在使用的Pin对象。
code/conversion/hybrid/ts/components/AddPinComponent.ts
constructor(@Inject('PinsService')private pinsService:PinsService,
@Inject('$state')private uiState:IStateService){
this.newPin = this.makeNewPin();
}
如前所述,我们用Inject在constructor中注入了这些服务,并且把this.newPin的值设置成了makeNewPin,也就是下面这个函数。
code/conversion/hybrid/ts/components/AddPinComponent.ts
makeNewPin():Pin {
return {
title:'Steampunk Cat',
description:'A cat wearing goggles',
user_name:'me',
avatar_src:'images/avatars/me.jpg',
src:'/images/pins/cat.jpg',
url:'http://cats.com',
faved:false,
id:Math.floor(Math.random()* 10000).toString()
};
}
这看起来很像AngularJS中的定义方式,不过这种方式现在的优点在于它是带类型信息的。
当用户提交表单时,我们调用onSubmit,其定义如下所示。
code/conversion/hybrid/ts/components/AddPinComponent.ts
onSubmit():void {
this.saving = true;
console.log('submitted', this.newPin);
setTimeout(()=> {
this.pinsService.addPin(this.newPin).then(()=> {
this.newPin = this.makeNewPin();
this.saving = false;
this.uiState.go('home');
});
}, 2000);
}
我们再次使用超时(timeout)技术来模拟通过向服务器发起请求来保存图钉的效果。这里我们使用的是setTimeout。下面对比一下在AngularJS中实现同样功能的写法。
code/conversion/AngularJS/js/app.js
ctrl.submitPin = function(){
ctrl.saving = true;
$timeout(function(){
PinsService.addPin(ctrl.newPin).then(function(){
ctrl.newPin = makeNewPin();
ctrl.saving = false;
$state.go('home');
});
}, 2000);
}
注意,我们在AngularJS中必须使用$timeout服务。为什么呢?这是因为AngularJS是基于摘要循环(digest loop)的。如果你在AngularJS中直接使用setTimeout,那么当调用回调函数时,它会处于Angular的控制范围之外。因此改动造成的影响不会扩散出来,除非某些代码触发了摘要循环(比如使用$scope.apply)。
然而在Angular中,你可以直接使用setTimeout,因为Angular中的变更检测使用的是Zones,所以更加自动化。你再也不用担心摘要循环了,这太好了!
在onSubmit中,我们通过下列代码调用了PinsService:
this.pinsService.addPin(this.newPin).then(()=> {
// ……
PinsService可以通过this.pinsService来访问,因为我们定义constructor时使用了特殊写法。编译器没有报错,这是因为我们已经在app.d.ts中声明过addPin接收一个Pin对象作为第一个参数。
code/conversion/hybrid/js/app.d.ts
export interface PinsService {
pins():Promise<Pin[]>;
addPin(pin:Pin):Promise<any>;
}
我们还把this.newPin定义成了一个Pin对象。
addPin解析完成后,我们把this.newPin重置为this.makeNewPin()的结果,并设置this.saving = false。
要返回首页,就要使用ui-router的$state服务。我们已经通过依赖注入把它存储到了this.uiState属性中,所以可以直接调用this.uiState.go('home')来变更状态。
16.6.11 使用AddPinComponent
我们现在就来使用AddPinComponent。
●降级Angular的AddPinComponent
要想使用AddPinComponent,就得先把它降级。
code/conversion/hybrid/ts/app.ts
angular.module('interestApp')
.directive('pinControls',
upgradeAdapter.downgradeNg2Component(PinControlsComponent))
.directive('addPin',
upgradeAdapter.downgradeNg2Component(AddPinComponent));
这会在AngularJS中创建一个addPin指令,它会匹配<add-pin>标签。
●路由到add-pin
为了使用这个新的AddPinComponent页,就要把它放进AngularJS应用中的某个地方。这很简单,只要让路由器拿到这个add状态,并把<add-pin>指令放到模板中就可以了。
code/conversion/hybrid/js/app.js
.state('add', {
template:"<add-pin></add-pin>",
url:'/add',
resolve:{
'pins':function(PinsService){
return PinsService.pins();
}
}
})
16.6.12 把Angular的服务暴露给AngularJS
目前,我们已经降级了Angular的组件使其能用在AngularJS中,还升级了AngularJS的服务使其能用在Angular中。但是当我们的应用开始升级到Angular时,可能会需要用TypeScript/Angular写一些服务,并把它暴露给AngularJS的代码。
那么我们就在Angular中创建一个简单的“分析”(analytics)服务,用来记录事件。
我们的想法是:在应用中有一个AnalyticsService,我们将调用它的recordEvent方法。在具体实现上,我们只会调用console.log来记录该事件,并把它存到一个数组中。这样做是为了把精力集中在最重要的事情上:描述如何把Angular的服务共享给AngularJS。
16.6.13 实现AnalyticsService
我们先来看看AnalyticsService的实现。
code/conversion/hybrid/ts/services/AnalyticsService.ts
import { Injectable } from '@angular/core';
/**
* Analytics Service records metrics about what the user is doing
*/
@Injectable()
export class AnalyticsService {
events:string[] = [];
public recordEvent(event:string):void {
console.log(`Event:${event}`);
this.events.push(event);
}
}
export var analyticsServiceInjectables:Array<any> = [
{ provide:AnalyticsService, useClass:AnalyticsService }
];
这里需要注意两点:recordEvent和Injectable。
recordEvent很简明:我们接收一个event:string参数,输出它的日志,并且把它保存到events中。在现实世界的应用中,你可能会把它发给某个外部服务,比如Google分析或Mixpanel。
要让该服务可注入,我们得做两件事:(1)为该类添加@Injectable注解;(2)把AnalyticsService这个令牌bind到该类。
现在,Angular将会管理该服务的单例对象,而我们可以把它注入到任何需要它的地方了。
16.6.14 把Angular的AnalyticsService降级到AngularJS
在AngularJS中使用AnalyticsService服务之前,我们需要把它降级。
把Angular服务降级到AngularJS的过程和指令的降级过程很相似,只不过多出了一个额外的步骤:得先确保AnayticsService出现在了我们这个NgModule的providers列表中。
code/conversion/hybrid/ts/app.ts
@NgModule({
declarations:[
PinControlsComponent,
AddPinComponent
],
imports:[
CommonModule,
BrowserModule,
FormsModule
],
providers:[
AnalyticsService,
]
})
class InterestAppModule { }
然后就可以使用downgradeNg2Provider了。
code/conversion/hybrid/ts/app.ts
angular.module('interestApp')
.factory('AnalyticsService',
upgradeAdapter.downgradeNg2Provider(AnalyticsService));
我们先调用angular.module('interestApp')来取得AngularJS的模块,然后像在AngularJS中一样调用.factory。要想降级该服务,要调用upgradeAdapter.downgradeNg2Provider(AnalyticsService)。它会把我们的AnalyticsService包装到一个函数中,而该函数会把它适配成一个AngularJS的工厂(factory)。
16.6.15 在AngularJS中使用AnalyticsService
现在就可以把Angular的AnalyticsService注入到AngularJS中去了。假如我们想记录HomeController是什么时候被访问的,就可以像下面这样来记录此事件。
code/conversion/hybrid/js/app.js
.controller('HomeController', function(pins, AnalyticsService){
AnalyticsService.recordEvent('HomeControllerVisited');
this.pins = pins;
})
这里注入了AnalyticsService,就像它是AngularJS中的普通服务一样,然后调用recordEvent。真棒!
我们可以在AngularJS中任何能使用依赖注入的地方使用该服务。比如,我们也可以把AnalyticsService注入到AngularJS的pin指令中。
code/conversion/hybrid/js/app.js
.directive('pin', function(AnalyticsService){
return {
restrict:'E',
templateUrl:'/templates/pin.html',
scope:{
'pin':"=item"
},
link:function(scope, elem, attrs){
scope.toggleFav = function(){
AnalyticsService.recordEvent('PinFaved');
scope.pin.faved =!scope.pin.faved;
}
}
}
})
现在我们掌握了把AngularJS应用升级到AngularJS/Angular混合式应用时所需的工具。AngularJS和Angular之间也有非常好的互操作性,这是因为Angular开发组付出了很多努力来对其进行简化。
AngularJS和Angular的指令与服务之间能够互通,让应用升级变得非常容易。当然,我们不可能一夜之间就把AngularJS的应用升级到Angular,不过UpgradeAdapter能让我们不必把那些老代码扔掉就开始使用Angular。
如果你想了解关于混合式Angular应用的更多知识,可以参阅下列资源。
●官方的Angular升级指南(中文版):https://angular.cn/docs/ts/latest/guide/upgrade.html
●Angular升级模块的单元测试:https://github.com/angular/angular/blob/master/modules/angular2/test/upgrade/upgrade_spec.ts
●Angular中DowngradeNg2ComponentAdapter的源代码:https://github.com/angular/angular/blob/master/modules/angular2/src/upgrade/downgrade_Angular_adapter.ts