如果你使用过一段时间的Angular,那么可能已经有了基于AngularJS的产品。Angular虽然很好,但我们总不能抛弃现有的一切,用Angular重写整个产品吧?更好的做法是对既有的AngularJS应用进行增量式升级。谢天谢地,Angular提供了一种非常棒的方式来实现它!

AngularJS和Angular的互操作性已经相当完善。在本章中,我们将讨论如何通过写混合式应用的方式来把AngularJS升级到Angular。这种混合式应用中同时运行着AngularJS和Angular框架(它们之间还可以交换数据)。

16.1 周边概念

当我们讨论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或担心变更检测方面的问题了。

16.2 我们要构建什么

在本章中,我们准备升级一个名叫Interest的应用,它模仿了Pinterest(如图16-1所示)。其思想在于你可以保存一枚图钉(pin),即一个带图片的链接。这些图钉会显示在列表中,而且你可以收藏(或取消收藏)一枚图钉。

图16-1 完成后的“山寨版”Pinterest

 你可以到code/conversion/AngularJS和code/conversion/hybrid下载AngularJS版和混合版的完整代码。

在深入讲解之前,我们先来看看AngularJS和Angular互操作的各种使用场景。

16.3 把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。

16.4 关于互操作性的需求

那么,有了这两种不同的体系,我们需要借助哪些特性来简化互操作性呢?

在AngularJS中使用Angular的组件:我们首先想到的是,要能写出新的Angular组件,并在AngularJS的应用中使用它们。

在Angular中使用AngularJS的组件:我们一般不会把整个组件树完全替换成Angular的组件,而是在Angular组件之中复用那些AngularJS组件。

服务共享:假设我们有一个UserService,想要在AngularJS和Angular之间共享它。服务通常就是一个普通的JavaScript对象,因此更抽象地说,我们需要的是一个能支持互操作的依赖注入系统。

变更检测:如果我们在某一边进行了改动,这些变更也应该能传播到另一边。

Angular提供了所有这些场景的解决方案,本章将一一讲解。

在本章中,我们会:

●描述即将升级的AngularJS应用;

●解释如何用Angular的UpgradeAdapter来组织混合式应用;

●通过把AngularJS应用转化成混合式应用来一步步解释如何在AngularJS和Angular中共享组件(指令)与服务。

16.5 AngularJS应用

作为准备,我们先重温一下该应用的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迁移。

16.6 构建混合式应用

现在,我们已经为往现有的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;

   }

  }

 }

})

16.7 总结

现在我们掌握了把AngularJS应用升级到AngularJS/Angular混合式应用时所需的工具。AngularJS和Angular之间也有非常好的互操作性,这是因为Angular开发组付出了很多努力来对其进行简化。

AngularJS和Angular的指令与服务之间能够互通,让应用升级变得非常容易。当然,我们不可能一夜之间就把AngularJS的应用升级到Angular,不过UpgradeAdapter能让我们不必把那些老代码扔掉就开始使用Angular。

16.8 参考资源

如果你想了解关于混合式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

  1. http://teropa.info/blog/2014/10/24/how-ive-improved-my-angular-apps-by-banning-ng-controller.html
  2. 视频地址:https://www.youtube.com/watch?v=gNmWybAyBHI>
  3. https://github.com/angular-ui/ui-router
  4. http://ng-book.com
  5. http://bower.io/