AngularJS Phonecat (步骤8-步骤9)

导言


最近在学AngularJS的实例教程PhoneCat Tutorial App,发现网上的中文教程都比较久远,与英文版对应不上,而且缺少组件和文件重构两节。所以决定自己整理一个中文简明教程。

此篇为8-9节。

上一篇:AngularJS Phonecat (步骤6-步骤7)

8 模板链接和图片


在这一步,我们要为手机列表增加缩略图和链接。

数据

phones.json文件保存了手机id和手机图片的url,url指向app/img/phones/文件夹。

app/phones/phones.json (其中一组数据):

[
  {
    ...
    "id": "motorola-defy-with-motoblur",
    "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
    "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
    ...
  },
  ...
]

模板

app/phone-list/phone-list.template.html:

...
<ul class="phones">
  <li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp" class="thumbnail">
    <a href="#/phones/{{phone.id}}" class="thumb">
      <img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
    </a>
    <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
    <p>{{phone.snippet}}</p>
  </li>
</ul>
...
  • {{phone.id}}绑定到<code>a</code>标签的<code>href</code>属性,{{phone.name}}绑定到<code>a</code>标签。
  • 引入手机图片,需要设置<code>img</code>标签。如果直接使用<code>src</code>绑定{{phone.imageUrl}},Angular未初始化前就会展开绑定,也就会发出无效的url请求。为此,我们使用<code>ngSrc</code>命令,它会在Angular初始化后再进行绑定。

端到端测试

e2e-tests/scenarios.js:

...

it('should render phone specific links', function() {
  var query = element(by.model('$ctrl.query'));
  query.sendKeys('nexus');
//点击链接
  element.all(by.css('.phones li a')).first().click();
//检查链接的url是否正确
  expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
});

...

该测试验证程序是否能正确生成图片链接。在命令行输入<code>npm run protractor</code>运行测试。

9 路由与多视图


这一步,我们会学习如何创建布局模板,如何利用Angular路由模块(ngRoute)实现多视图。
当你在浏览器中输入/index.html。会重定向到/index.html#!/phones并显示手机列表。当点击手机链接时,会转到手机详情页面。

依赖

路由功能由<code>ngRoute</code>模块提供,这是一个独立于Angular核心框架的模块。

我们使用Bower来安装客户端依赖。更新bower.json配置文件来包含新的依赖关系:

bower.json:

{
  "name": "angular-phonecat",
  "description": "A starter project for AngularJS",
  "version": "0.0.0",
  "homepage": "https://github.com/angular/angular-phonecat",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "angular": "1.5.x",
    "angular-mocks": "1.5.x",
    "angular-route": "1.5.x",
    "bootstrap": "3.3.x"
  }
}

指明"angular-route": "1.5.x",会让bower安装1.5.x版本的angular-route模块。

如果你是在全局环境中安装bower,你可以使用<code>bower install</code>进行安装。而这个项目我们已经预配使用npm来运行bower install,所以只需要输入命令行:

npm install

多视图、路由与布局模板

我们的程序逐渐增大,也变得更复杂。在上一节,我们只有一个的视图(用来显示手机列表),所有的模板代码都放置在phone-list.template.html中。这一节,我们会添加一个视图来展示手机详情。

为了添加详情页,我们会将index.html转换成布局模板(layout template),在所有页面中通用。其他局部模板(partial templates)会通过当前路由引入到布局模板中(当前路由决定引入哪个局部模板)。

Angular通过$routeProvider声明路由,它是$route服务的提供者。该服务会将控制器、视图模板、当前url连接起来。它还能实现深层链接:浏览器历史(后退/前进)、收藏标签。

ngRoute让控制器和特定url的模板关联起来。具体方法是:将组件关联到路由上,组件作为提供者负责提供视图模板和控制器。

关于依赖注入(DI):注入器(Injector)和提供者(Providers)

一般来说,一个对象只能通过三种方法来得到它的依赖项目:

  • 在对象内部创建依赖项目
  • 将依赖作为一个全局变量来进行查找或引用
  • 将依赖传递到需要它的地方
    第一种方法无法隔离对象和依赖项目,第二种方法则容易污染全局作用域。所以我们使用第三种方法,即依赖注入。依赖注入是一种设计模式,它移除了硬编码依赖,因此使得我们可以在运行中随时移除并改变依赖项目。这对于测试也很有好处,我们可以用测试环境中的一个模拟对象来替换生产环境中的一个真实对象。

依赖注入(DI)也是AngularJS的核心,所以我们要了解其基本原理。

1)Angular为什么需要依赖注入?
AngularJS的组件之间无法互相直接调用,一个组件必须通过注入器调用另一个组件。这样的好处是组件之间相互解耦,对象整个生命周期的管理都丢给了注入器。

2)Angular如何实现依赖注入?
程序启动时,Angular会创建一个注入器(injector),用于查找和注入程序所需的服务。注入器本身并不了解服务($http、$route)能做什么,甚至服务是否存在(除非服务配置在适当的模块定义中),它只是存放服务的容器。

注入器:

  • 加载依赖:加载程序依赖的模块定义
  • 注册依赖:注册模块定义的提供者
  • 注入依赖:当有实际的请求时,注入器通过提供者(注:提供者作为参数注入函数)实例化具体的服务及对应的依赖。

提供者(一般指组件):
提供者是提供(创建)服务实例的对象,它的配置API可用来控制服务的创建和运行的行为。比如$route服务,$routeProvider暴露的API允许你为程序定义路由。

注意:提供者只能注入到config函数,因此你不能把$routeProvider注入PhoneListController

Angular模板解决了全局变量的问题。与AMD或者require.js模块不同,Angular模板不需处理脚本加载顺序和延迟获取等问题,这些目标是独立的模块系统,他们可以并列存在和实现自己的目标。

要深入理解Angular的依赖注入,请参阅Understanding Dependency Injection

模板

$route服务通常与ngView指令联合使用。ngView指令会将当前路由的视图模板加到布局模板中,这让index.html模板看起来更简洁。

app/index.html:

<head>
  ...
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="app.module.js"></script>
  <script src="app.config.js"></script>
  ...
  <script src="phone-detail/phone-detail.module.js"></script>
  <script src="phone-detail/phone-detail.component.js"></script>
</head>
<body>

  <div ng-view></div>

</body>

我们在index.html中增加了4个新的<code><script></code>标签,用于加载额外的JS脚本。

  • angular-route.js: 定义ngRoute模块,用于提供路由
  • app.config.js: 配置主模块所需的提供者。
  • phone-detail.module.js: 定义一个包含手机详情组件的新模块。
  • phone-detail.component.js: 定义一个手机详情的组件。
    注意,我们移除了<code><phone-list></phone-list></code>,增加了含ng-view属性的<code>div</code>元素。
tutorial_09.png

配置模板

模板的.config() 方法可以获取配置所需的提供者。
为了获取ngRoute中的的提供者、服务和指令,我们需要为phonecatApp增加ngRoute模块。

app/app.module.js:

angular.module('phonecatApp', [
  'ngRoute',  //增加ngRoute模块
  ...
]);

除了核心服务和指令,我们还需要配置$route服务(使用它的提供者)。

app/app.config.js:

angular.
  module('phonecatApp').
  config(['$locationProvider', '$routeProvider', 
    function config($locationProvider, $routeProvider) {
      $locationProvider.hashPrefix('!');
    //配置$route服务
      $routeProvider.
        when('/phones', {
          template: '<phone-list></phone-list>'
        }).
        when('/phones/:phoneId', {
          template: '<phone-detail></phone-detail>'
        }).
        otherwise('/phones');
    }
  ]);

通过.config() 方法,我们请求提供者(如$routeProvider)注入到配置函数中,并使用提供者的方法指定服务行为。在这里,我们使用$routeProvider.when()和$routeProvider.otherwise() 制定程序路由的选择规则。

我们还使用$locationProvider.hashPrefix()将哈希前缀设置为!,该前缀会加到我们客户端路由链接中,位于文件路径与字符(#)之间(例如 index.html#!/some/path)。虽然设置前缀不是必须的,但这是种好的做法(具体原因超出本课程范围,不做讨论)。!是最常用的前缀。

我们的定义了如下路由:

  • when('/phones'):当url以/phones结尾时,显示手机列表。为了构造这个视图,Angular会创建一个phoneList组件的实例来管理视图。注意,我们在index.html中也使用了相同的标记(/phones)。
  • when('/phones/:phoneId'): 当url以/phones/:phoneId结尾时,显示手机详情。:phoneId是一个URL变量片段。该视图由phoneDetail组件管理。
  • otherwise('/phones'): 当前url没有找到匹配的路由时,重定向到/phones页面。

我们再次使用了phoneList组件,又添加了一个新的组件phoneDetail。现在phoneDetail组件只是显示选择的手机ID(不太吸引人,我们下一节会增强该组件)。

注意第2个路由声明中的:phoneId参数。$route服务使用路由声明(/phones/:phoneId)作为一个模板来匹配当前的URL。所有:phoneId变量都会被提取到$routeParams对象中。

phoenDetail组件

phoenDetail组件用于处理手机详情视图。遵循和phoneList一样的规则,我们使用一个单独文件夹,创建一个phoneDetail模块,并将该模块作为依赖加到主模块phoneact中。

app/phone-detail/phone-detail.module.js 详情模块:

angular.module('phoneDetail', [
  'ngRoute'
]);

app/phone-detail/phone-detail.component.js 详情组件:

angular.
  module('phoneDetail').
  component('phoneDetail', {
    template: 'TBD: Detail view for <span>{{$ctrl.phoneId}}</span>',
    controller: ['$routeParams',
      function PhoneDetailController($routeParams) {
        this.phoneId = $routeParams.phoneId;
      }
    ]
  });

app/app.module.js 主模块:

angular.module('phonecatApp', [
  ...
  'phoneDetail',
  ...
]);

子模块依赖的注意事项

phoneDetail模块依赖于ngRoute模块:ngRoute提供$ routeParams对象,并将$ routeParams对象注入到phoneDetail组件的控制器。由于ngRoute也是phonecatApp主模块的依赖,所以服务和指令在程序中随处可用(包括phoneDetail组件)。

这意味着,即使我们没有将ngRoute放入phoneDetail组件的依赖列表中,我们的程序仍然可以正常运作。虽然省略子模块依赖(这些依赖已经导入主模块)听起来挺好的,但它打破程序的模块化,所以并不可取。

想象一下,我们现在要在新项目中使用phoneDetail功能,但新项目并没有声明ngRoute依赖。这样,注入器就无法提供$routeParams,新程序也就无法运行了。

所以,我们要始终明确一个子模块的依赖。不要依靠从父模块继承的依赖(因为父模块某天可能就不存在了)。

在多个模块声明相同的依赖不会产生额外的“成本”,因为每个依赖依然会加载一次。有关模块及其依赖的详细信息请参阅module

测试

我们的部分模块依赖与ngRoute,所以我们需要在Karma配置文件添加angular-route。

karma.conf.js:

files: [
  'bower_components/angular/angular.js',
  'bower_components/angular-route/angular-route.js',
  ...
],

使用端到端测试导航到各个URL,验证程序是否能正确渲染视图:

e2e-tests/scenarios.js

...

it('should redirect `index.html` to `index.html#!/phones', function() {
  browser.get('index.html');
  expect(browser.getLocationAbsUrl()).toBe('/phones');
});

...
//导航到/phones
describe('View: Phone list', function() {

  beforeEach(function() {
    browser.get('index.html#!/phones');
  });

  ...

});

...
//导航到/phones/nexus-s
describe('View: Phone details', function() {

  beforeEach(function() {
    browser.get('index.html#!/phones/nexus-s');
  });

  it('should display placeholder page with `phoneId`', function() {
    expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s');
  });

});

...

在命令行中输入<code>npm run protractor</code>就可以运行测试了。

下一篇:AngularJS Phonecat (步骤10-步骤12)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容