【一起学AngularJS】第九章、多视图和视图路由

本章我们将一起学习:

  • 如何创建一个布局模版
  • 如何如何创建一个多视图路由应用
  • 如何使用ngRoute指令实现视图路由

我们先看看本章对应的例子要做的事情是什么:

  • 当你点击导航栏导航到app/index.html时,你将被重定向到app/index.html/#/phones,它会显示手机列表。
  • 当你点击一个手机链接时,URL地址栏的地址变化(显然),新的页面将显示手机的一些信息。

【注解】本章需要一定Provider提供者的知识,可以先看本章教程,如果看完之后想系统学习下提供者、服务相关的知识,点击AngularJS服务体系查看。

下面我们把代码切换到step-7:

git checkout -f step-7

假设你已经运行了网站,你只需要刷新你的浏览器来看效果。或者你可以在线看效果

相关依赖

我们通过ngRoute指令实现视图路由技术,它不属于Angular核心框架。
我们将使用Bower来安装客户端依赖。以下是新的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.4.x",
    "angular-mocks": "1.4.x",
    "jquery": "~2.2.1",
    "bootstrap": "~3.1.1",
    "angular-route": "1.4.x"
  }
}

"angular-route": "1.4.x" 这句话告诉bower安装1.4.x版本的Angular路由插件。我们需要保证路由插件的成功安装。
如果你已经全局安装了bower管理器,你可以通过bower install来安装所有的依赖。但是对于本项目来说,我们已经在npm脚本中配置了bower install的执行,所以我们只需要运行:

npm install

注意:如果你运行上述命令后,恰好一个新的Angular版本发布了,这时候,bower install命令可能会执行失败,因为新旧版本的AngularJS有冲突。如果你遇到了这个问题,你只需要删除你的app/bower_components文件夹,再次运行npm install

多视图,路由技术和布局模版

我们的应用内容越来越丰富了。在本章之前,应用只有一个视图(手机列表),并且所有的模版代码都在index.html文件中。下面我们将为列表中的每一个手机添加一个详情视图。
我们可以直接把详情视图添加到inde.html中,但是这样做会让index.html变得特别大,看起来会很糟糕。所以我们换一种做法,我们将把index.html分解成布局模版。这个模版适用于本应用中的所有页面(注解:这和sitemesh、velocity、apache Tiles等服务端模版技术对应)。其他的局部模版将根据当前的路由被选择性的集成到当前模版中,从而最终呈现给用户。
Angular使用$routeProvider来申明应用应用路由,它是$route service的提供者。这个服务可以很容易的把控制器、视图模版和当前的URL地址绑定在一起。通过这个特性,我们可以实现deep linking,通过它我们可以利用浏览器的历史功能(往后和前进)以及书签功能。

探讨下依赖注入、注入器和提供者

依赖注入功能是AngularJS的核心功能,所以了解一点它的知识是有必要的。
当应用启动时,AngularJS将先创建一个注入器,它将寻找并且注入应用所需要的所有服务。注入器本身是不知道$http或者$route服务是干嘛的。实时上,注入器都不知道它们的存在,除非他们被依赖进来。
注入器只完成以下几个步骤:

  • 加载应用中指定的模块定义
  • 注册模块定义中所有的提供器(提供者)——配置阶段
  • 当需要的时候,注入服务或者提供者延迟初始化的依赖等。——运行阶段

提供者的功能为创建服务实例和暴露配置API,后者包括控制服务的创建和运行时行为。在$route服务中,$routeProvider暴露一些允许你定义路由的API。

注意:提供者只能在config函数里被注入。所以你无法把$routeProvider注入到PhoneListCtrl中。

Angular模块解决了移除应用全局状态的问题,并且提供了一个配置注入器的方式。跟AMD或者require.js不一样的是,Angular模块不会去解决脚本加载顺序或者延迟脚本获取的问题。这些目标是相互独立的,他们可以共同工作于一个系统中,各自完成自己的目标(意思就是AngularJS和AMD或者require.js框架可以公用)。
如果你想进一步了解Angular中的依赖注入,可以看看这里:理解依赖注入.

模版

$route指令通常和$ngView指令结合使用。$ngView指令所扮演的角色是把当前路由对应的视图模版加入布局模版中。所以这里使用它正好。

注意:从AngularJS 1.2起,ngRoute就是Angular的自有模块了,而且必须通过加载另外的angular-route.js文件来完成载入(由Bower下载)

app/index.html

<!doctype html>
<html lang="en" ng-app="phonecatApp">
<head>
...
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="js/app.js"></script>
  <script src="js/controllers.js"></script>
</head>
<body>

  <div ng-view></div>

</body>
</html>

这段代码中,我们新增了两个<script>标签,它们是用来加载额外的Javascript的:

  • angular-route.js: 定义了Angular ngRoute模块,将提供我们路由操作。
  • app.js: 这个文件包含了我们的根模块。

可以注意到这个index.html模版中的大部分代码已经被移出了,取而代之的是一个含有ng-view属性的<div>标签。被移出去的部分我们重新放在了phone-list.html模版。
app/partials/phone-list.html:

<div class="container-fluid">
  <div class="row">
    <div class="col-md-2">
      <!--Sidebar content-->

      Search: <input ng-model="query">
      Sort by:
      <select ng-model="orderProp">
        <option value="name">Alphabetical</option>
        <option value="age">Newest</option>
      </select>

    </div>
    <div class="col-md-10">
      <!--Body content-->

      <ul class="phones">
        <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
          <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
          <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
          <p>{{phone.snippet}}</p>
        </li>
      </ul>

    </div>
  </div>
</div>

同样的,我们也为手机详情视图创建一个占位模版:
app/partials/phone-detail.html:

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

注意这里我们是如何使用phoneId(将在PhoneDetailCtrl控制器中被定义)表达式的。

本章我们将一起学习:

  • 如何创建一个布局模版
  • 如何如何创建一个多视图路由应用
  • 如何使用ngRoute指令实现视图路由

我们先看看本章对应的例子要做的事情是什么:

  • 当你点击导航栏导航到app/index.html时,你将被重定向到app/index.html/#/phones,它会显示手机列表。
  • 当你点击一个手机链接时,URL地址栏的地址变化(显然),新的页面将显示手机的一些信息。

下面我们把代码切换到step-7:

git checkout -f step-7

假设你已经运行了网站,你只需要刷新你的浏览器来看效果。或者你可以在线看效果

相关依赖

我们通过ngRoute指令实现视图路由技术,它不属于Angular核心框架。
我们将使用Bower来安装客户端依赖。以下是新的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.4.x",
    "angular-mocks": "1.4.x",
    "jquery": "~2.2.1",
    "bootstrap": "~3.1.1",
    "angular-route": "1.4.x"
  }
}

"angular-route": "1.4.x" 这句话告诉bower安装1.4.x版本的Angular路由插件。我们需要保证路由插件的成功安装。
如果你已经全局安装了bower管理器,你可以通过bower install来安装所有的依赖。但是对于本项目来说,我们已经在npm脚本中配置了bower install的执行,所以我们只需要运行:

npm install

注意:如果你运行上述命令后,恰好一个新的Angular版本发布了,这时候,bower install命令可能会执行失败,因为新旧版本的AngularJS有冲突。如果你遇到了这个问题,你只需要删除你的app/bower_components文件夹,再次运行npm install

多视图,路由技术和布局模版

我们的应用内容越来越丰富了。在本章之前,应用只有一个视图(手机列表),并且所有的模版代码都在index.html文件中。下面我们将为列表中的每一个手机添加一个详情视图。
我们可以直接把详情视图添加到inde.html中,但是这样做会让index.html变得特别大,看起来会很糟糕。所以我们换一种做法,我们将把index.html分解成布局模版。这个模版适用于本应用中的所有页面(注解:这和sitemesh、velocity、apache Tiles等服务端模版技术对应)。其他的局部模版将根据当前的路由被选择性的集成到当前模版中,从而最终呈现给用户。
Angular使用$routeProvider来申明应用应用路由,它是$route service的提供者。这个服务可以很容易的把控制器、视图模版和当前的URL地址绑定在一起。通过这个特性,我们可以实现deep linking,通过它我们可以利用浏览器的历史功能(往后和前进)以及书签功能。

探讨下依赖注入、注入器和提供者

依赖注入功能是AngularJS的核心功能,所以了解一点它的知识是有必要的。
当应用启动时,AngularJS将先创建一个注入器,它将寻找并且注入应用所需要的所有服务。注入器本身是不知道$http或者$route服务是干嘛的。实时上,注入器都不知道它们的存在,除非他们被依赖进来。
注入器只完成以下几个步骤:

  • 加载应用中指定的模块定义
  • 注册模块定义中所有的提供器(提供者)——配置阶段
  • 当需要的时候,注入服务或者提供者延迟初始化的依赖等。——运行阶段

提供者的功能为创建服务实例和暴露配置API,后者包括控制服务的创建和运行时行为。在$route服务中,$routeProvider暴露一些允许你定义路由的API。

注意:提供者只能在config函数里被注入。所以你无法把$routeProvider注入到PhoneListCtrl中。

Angular模块解决了移除应用全局状态的问题,并且提供了一个配置注入器的方式。跟AMD或者require.js不一样的是,Angular模块不会去解决脚本加载顺序或者延迟脚本获取的问题。这些目标是相互独立的,他们可以共同工作于一个系统中,各自完成自己的目标(意思就是AngularJS和AMD或者require.js框架可以公用)。
如果你想进一步了解Angular中的依赖注入,可以看看这里:理解依赖注入.

模版

$route指令通常和$ngView指令结合使用。$ngView指令所扮演的角色是把当前路由对应的视图模版加入布局模版中。所以这里使用它正好。

注意:从AngularJS 1.2起,ngRoute就是Angular的自有模块了,而且必须通过加载另外的angular-route.js文件来完成载入(由Bower下载)

app/index.html

<!doctype html>
<html lang="en" ng-app="phonecatApp">
<head>
...
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="js/app.js"></script>
  <script src="js/controllers.js"></script>
</head>
<body>

  <div ng-view></div>

</body>
</html>

这段代码中,我们新增了两个<script>标签,它们是用来加载额外的Javascript的:

  • angular-route.js: 定义了Angular ngRoute模块,将提供我们路由操作。
  • app.js: 这个文件包含了我们的根模块。

可以注意到这个index.html模版中的大部分代码已经被移出了,取而代之的是一个含有ng-view属性的<div>标签。被移出去的部分我们重新放在了phone-list.html模版。
app/partials/phone-list.html:

<div class="container-fluid">
  <div class="row">
    <div class="col-md-2">
      <!--Sidebar content-->

      Search: <input ng-model="query">
      Sort by:
      <select ng-model="orderProp">
        <option value="name">Alphabetical</option>
        <option value="age">Newest</option>
      </select>

    </div>
    <div class="col-md-10">
      <!--Body content-->

      <ul class="phones">
        <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
          <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
          <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
          <p>{{phone.snippet}}</p>
        </li>
      </ul>

    </div>
  </div>
</div>

同样的,我们也为手机详情视图创建一个占位模版:
app/partials/phone-detail.html:

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

注意这里我们是如何使用phoneId(将在PhoneDetailCtrl控制器中被定义)表达式的。

应用模块

为了改善应用的组织结构,我们使用了Angular的ngRoute模块,并且我们把控制器移动到了它们自己的模块phonecatControllers(如下所示):
app/js/app.js:

var phonecatApp = angular.module('phonecatApp', [
  'ngRoute',
  'phonecatControllers'
]);
...

我们把angular-route.js文件引入了index.html,并且在controllers.js中新建了一个phonecatControllers模块。然而这些该不足以让我们可以使用ngRoute模块和phonecatControllers模块。我们还需要把他们作为应用的依赖引入进来。像上述代码一样,我们把这两个模块引入本应用,这样我们就可以使用它们提供的指令和服务了。
注意上述代码中第二个参数“['ngRoute', 'phonecatControllers']”,是一个字符串数组,代表了phonecatApp的依赖。

...

phonecatApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html',
        controller: 'PhoneListCtrl'
      }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html',
        controller: 'PhoneDetailCtrl'
      }).
      otherwise({
        redirectTo: '/phones'
      });
  }]);

通过phonecatApp.config()方法,我们把$routeProvider注入了配置函数,并且允许我们使用$routeProvider.when()方法来定义我们的路由。
我们的应用路由定义如下:

  • when('/phones'):当URL哈希片段值为/phones时,手机列表视图将会被展示。Angular将使用phone-list.html模版和PhoneListCtrl控制器来完成视图的展示。
  • when('/phones/:phoneId'): 当URL的哈希片段为/phones/:phoneId时,手机详情视图将被展示。其中,phoneId是URL中变化的部分。Angular将使用phone-detail.html模版和PhoneDetailCtrl控制器完成视图的展示。
  • otherwise({redirectTo: '/phones'}): 其他情况下,将跳转到手机列表页。

为了展示手机详情视图的展示,我们重用了前面几章中创建的PhoneListCtrl控件,并且添加了一个新的PhoneDetailCtrl控制器。这些改变都在app/js/controllers.js中。
我们来看看第二个路由申明中的:phoneId参数。$route服务通过/phones/:phoneId这样的路由申明来匹配当前的URL。:标记的变量值都会被抽取并且赋值给$routeParams对象。

控制器

app/js/controllers.js

var phonecatControllers = angular.module('phonecatControllers', []);

phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
  function ($scope, $http) {
    $http.get('phones/phones.json').success(function(data) {
      $scope.phones = data;
    });

    $scope.orderProp = 'age';
  }]);

phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
  function($scope, $routeParams) {
    $scope.phoneId = $routeParams.phoneId;
  }]);

可以看到,我们又创建一个叫phonecatControllers的模块。对于小型的AngularJS应用来说,把所有的少量的控制器放在一个模块里时很常见的。但是当我们的应用越来越大,把代码整理到新的模块中去也是很常见的。对于大型的应用来说,你很可能为应用的每个较大的功能分别设计一个模块。
目前我们的应用还很小,所以都放在phonecatControllers模块中也是可以理解的。

测试

为了自动测试所有东西都被成功绑定了,我们需要写一些E2E(端到端)的测试用例来验证不通的URL可以得到正确的视图渲染。

...
   it('should redirect index.html to index.html#/phones', function() {
    browser.get('app/index.html');
    browser.getLocationAbsUrl().then(function(url) {
        expect(url).toEqual('/phones');
      });
  });

  describe('Phone list view', function() {
    beforeEach(function() {
      browser.get('app/index.html#/phones');
    });
...

  describe('Phone detail view', function() {

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


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

然后通过npm run protractor命令来观察测试运行。

实验小能手

{{orderProp}}数据绑定加入到index.html,你会发现什么都没变,在手机列表视图中也一样。这是因为orderProp模型的属于PhoneListCtrl域,对应绑定的HTML元素是<div ng-view>。如果你把它加入到phone-list.html模版中,就能正常工作了。

总结

我们已经实现了多视图和路由功能,下面一章我们将实现详细的手机详情视图。

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

推荐阅读更多精彩内容