步骤7——路由与多视图

这一步,将学习到如何创建布局模板,以及利用Angular路由模块(ngRoute)实现的多视图应用。 当浏览器导航到app/index.html时,会重定向到app/index.html#/phones来访问呈现一个手机列表。 当点击手机链接时,URL会改变指向到一款具体的手机,并且显示其详细的信息页面。

工作区切换到步骤7

直接用浏览器访问步骤7在线演示

大多数的重要改变都会列在下面,不过你也可以在GitHub看到完整的差异。

依赖

这一步添加的路由功能是由Angular的ngRoute模块提供,这是一个独立于Angular核心框架的模块。

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

  1. {
  2. "name": "angular-seed",
  3. "description": "A starter project for AngularJS",
  4. "version": "0.0.0",
  5. "homepage": "https://github.com/angular/angular-seed",
  6. "license": "MIT",
  7. "private": true,
  8. "dependencies": {
  9. "angular": "1.2.x",
  10. "angular-mocks": "~1.2.x",
  11. "bootstrap": "~3.1.1",
  12. "angular-route": "~1.2.x"
  13. }
  14. }

新的依赖关系"angular-route": "~1.2.x"告诉bower安装版本1.2.x的angular-route组件。我们必须明确告诉bower下载安装这个依赖。

如果你是在全局环境中使用bower安装,你可能会用直接到bower install指令,但是在这个项目中我们已经预配使用npm来启动bower install,所以你只需要: npm install

多视图、路由与布局模板

我们的程序逐渐强大,也变得更加复杂。在步骤7(本步骤)之前,我们只有唯一的视图来(用来显示手机列表),并且所有的模板代码都放置在index.html中。新的步骤中会添加一个视图来显示列表中每个设备详细的信息(详细说明视图)。

为了添加详细说明视图,我们扩展index.html模板文件来包含两个视图,但这将很快引起混乱,为了替代,我们尝试把index.html转换到我们称为布局模板(layout template)其中有模板(布局模板)在所有视图中通用,其他则是局部模板(partial templates),局部模板只包括当前路由route——视图当前显示需要的部分。

在Angular中,程序的路由通过$routeProvider声明,它是$route服务的提供者。这个服务能容易的把控制器、视图模板和浏览器当前的地址栏信息连接起来。使用这个特性,我们可以实现深层链接(deep linking),它可以让你可以利用浏览器历史(后退和前进)以及收藏标签。

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

是否注意到,依赖注入(DI)是AngularJS的核心,所以了解一下其是如何实现的是十分重要的。

当程序启动时,Angular创建一个注入器用于查找和注入程序需要的服务。注入器自身是不知道任何关于$http或者$route服务能做什么的,事实上注入器都不知道这些服务是否存在(除非服务配置在适当的模块定义中)。

注入器只执行以下步骤:

  • 加载描述在程序中的模块定义
  • 注册所有模块定义的提供者
  • 当有实际的请求时,注入器检测具体的功能要求及对应的依赖(服务),再通过其提供者(延时)实例化来完成功能的提供

提供者是一种可以提供(创建)服务实例的对象,其还可以通过配置的API暴露调用接口,用于控制的建立和运行时行为功能。实际上这$route服务、$routeProvider暴露的API允许为你的程序定义路由。

注意:提供者只能注入到config功能,因此你不能把$routeProvider注册入$PhoneListCtrl

Angular模块从应用程序中解决(去除了)全局状态的问题,并提供一个配置注入器的方法。与AMD或者require.js模块截然相反,Angular模块不会尝试处理脚本的加载和延时获取等问题。这些目标是完全独立的模块系统,它们可以并列存在和实现他们的目标。

要深入理解Angular的依赖注入,观看Understanding Dependency Injection

模板

$route服务通常与ngView指令联合使用。ngView指令规包含了视图模板如何路由到的布局视图。这使得index.html模板显得更加完美。

注意:从AngularJS版本1.2开始ngRoute是独立的模块,需要单独加载angular-route.js文件,这可以通过bower下载到。

app/index.html

  1. <!doctype html>
  2. <html lang="en" ng-app="phonecatApp">
  3. <head>
  4. ...
  5. <script src="bower_components/angular/angular.js"></script>
  6. <script src="bower_components/angular-route/angular-route.js"></script>
  7. <script src="js/app.js"></script>
  8. <script src="js/controllers.js"></script>
  9. </head>
  10. <body>
  11. <div ng-view></div>
  12. </body>
  13. </html>

我们添加了2个新的<script>标签以在我们的index文件中加载额外的JavaScript文件:

  • angular-route.js:定义了Angular的ngRoute模块,用于提供路由功能
  • app.js:这个文件在程序中提供一个root模块。

注意我们从index.html模板文件中移除了大部分代码,取而代之的是一个定义了ng-view属性的div标签。移除的代码被放置到phone-list.html模板文件中: app/partials/phone-list.html:

  1. <div class="container-fluid">
  2. <div class="row">
  3. <div class="col-md-2">
  4. <!--Sidebar content-->
  5. Search: <input ng-model="query">
  6. Sort by:
  7. <select ng-model="orderProp">
  8. <option value="name">Alphabetical</option>
  9. <option value="age">Newest</option>
  10. </select>
  11. </div>
  12. <div class="col-md-10">
  13. <!--Body content-->
  14. <ul class="phones">
  15. <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
  16. <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
  17. <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
  18. <p>{{phone.snippet}}</p>
  19. </li>
  20. </ul>
  21. </div>
  22. </div>
  23. </div>

我们还增加了一个包含占位符的手机详细说明视图的模板: app/partials/phone-detail.html:

  1. TBD:detail view for <span>{{phoneId}}</span>

注意现在我们使用了phoneId表达式,它是定义在PhoneDetailCtrl控制器中的。

程序模块

为了改善程序的组织,我们使用Angular的ngRoute·模块,并且把控制器移动到自己的模块phonecatControllers`(下面会看见)。

我们添加angular-route.jsindex.html中,并添加了一个新的phonecatControllers模块到controllers.js中。这些不是所有我们会用到的代码,我们还要添加一些我们程序依赖的模块。下面两个模块都是phonecatApp需要的,我们可用指令和服务提供。

app/js/app.js:

  1. var phonecatApp = angular.module('phonecatApp', [
  2. 'ngRoute',
  3. 'phonecatControllers'
  4. ]);
  5. ...

注意传递给angular.module的第2个参数['ngRoute','phonecatControllers'],这个数组列出了phonecatApp依赖的模块。

  1. ...
  2. phonecatApp.config(['$routeProvider',
  3. function($routeProvider) {
  4. $routeProvider.
  5. when('/phones', {
  6. templateUrl: 'partials/phone-list.html',
  7. controller: 'PhoneListCtrl'
  8. }).
  9. when('/phones/:phoneId', {
  10. templateUrl: 'partials/phone-detail.html',
  11. controller: 'PhoneDetailCtrl'
  12. }).
  13. otherwise({
  14. redirectTo: '/phones'
  15. });
  16. }]);

通过phonecatApp.config()方法,我们请求$routeProvider注入自己配置的函数,并且使用$routeProvider.when()方法提供了自定义路由规则。

我们的程序定义了如下的路由:

  • 匹配/phones :当URL结尾片段是/phones时显示一个手机列表。为了构造这个输出视图,Angular会使用phone-list.html模板和PhoneListCtrl控制器
  • 匹配/phones/:phoneId: 当URL结尾片段匹配/phones/:phoneId时会显示对应手机的详细说明视图。这里:phoneId是一个URL变量区块。为了生成这个手机详细说明视图,Angular使用phone-detail.html模板和PhoneDetailCtrl控制器。
  • 其他的匹配(重定向到/phones):当浏览器地址栏信息不能匹配到有效路由(自定义的路由设置没有对应项目)时,尝试重定向到/phones

我们再次使用了上一步骤的PhoneListCtrl控制器,然后添加了一个新的(但是是空的)PhoneDetailCtrl控制器到app/js/controllers.js文件中来为手机详细说明视图提供数据。

注意在第2个路由声明中的:phoneId参数。$route服务使用路由声明——/phones/:phoneId——作为一个模板来匹配当前的URL。所有的这些变量定义都会在$routeParams对象中展开。

控制器

app/js/controllers.js:

  1. var phonecatControllers = angular.module('phonecatControllers', []);
  2. phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
  3. function ($scope, $http) {
  4. $http.get('phones/phones.json').success(function(data) {
  5. $scope.phones = data;
  6. });
  7. $scope.orderProp = 'age';
  8. }]);
  9. phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
  10. function($scope, $routeParams) {
  11. $scope.phoneId = $routeParams.phoneId;
  12. }]);

再次提醒,我们是创建了一个新的模块叫做phonecatControllers。对于很多小的AngularJS程序,共同创造是一个模块为所有控制器(如果有几个)公用。随着程序的增强,很容易为程序添加更多的公用模块。对于大的程序,你需要为每个主要的程序功能创建特定的模块。

因为我们的例子程序是相对比较小的程序,所以我们把所有的控制器都添加到phoecatControllers模块中。

测试

为了自动验证所有的可能,我们写端到端测试来导航到各式URL,并且验证是否能显示正确:

  1. ...
  2. it('should redirect index.html to index.html#/phones', function() {
  3. browser.get('app/index.html');
  4. browser.getLocationAbsUrl().then(function(url) {
  5. expect(url.split('#')[1]).toBe('/phones');
  6. });
  7. });
  8. describe('Phone list view', function() {
  9. beforeEach(function() {
  10. browser.get('app/index.html#/phones');
  11. });
  12. ...
  13. describe('Phone detail view', function() {
  14. beforeEach(function() {
  15. browser.get('app/index.html#/phones/nexus-s');
  16. });
  17. it('should display placeholder page with phoneId', function() {
  18. expect(element(by.binding('phoneId')).getText()).toBe('nexus-s');
  19. });
  20. });

你可以用npm run protractor来运行并观察测试结果。

尝试

试着在index.html中添加一个{{orderProp"}}的绑定观察点,你会发现当手机列表正常显示时,你看不到任何相关信息,这是因为现在orderProp数据模型只在PhoneListCtrl模型中的作用范围内有效(可见)。为了关联

元素,你需要添加相同绑定到phone-list.html模板中,这个绑定将正常工作。

小结

这一步,我们建立了路由,并实现了手机列表视图,接下来的步骤8中,我们将实现手机详细说明视图。