在這一步,你將學習如何創建一個布局模板并且通過路由功能來構建一個具有多個視圖的應用。
請重置工作目錄:
~~~
git checkout -f step-7
~~~
注意到現在當你轉到`app/index.html`時,你會被重定向到`app/index.html#/phones`并且相同的手機列表在瀏覽器中顯示了出來。當你點擊一個手機鏈接時,一個手機詳細信息列表也被顯示了出來。
步驟6和步驟7之間最重要的不同在下面列出。你可以在[GitHub](https://github.com/angular/angular-phonecat/compare/step-6...step-7)里看到完整的差別。
## 多視圖,路由和布局模板
我們的應用正慢慢發展起來并且變得逐漸復雜。在步驟7之前,應用只給我們的用戶提供了一個簡單的界面(一張所有手機的列表),并且所有的模板代碼位于`index.html`文件中。下一步是增加一個能夠顯示我們列表中每一部手機詳細信息的頁面。
為了增加詳細信息視圖,我們可以拓展`index.html`來同時包含兩個視圖的模板代碼,但是這樣會很快給我們帶來巨大的麻煩。相反,我們要把`index.html`模板轉變成“布局模板”。這是我們應用所有視圖的通用模板。其他的“局部布局模板”隨后根據當前的“路由”被充填入,從而形成一個完整視圖展示給用戶。
AngularJS中應用的路由通過[$routeProvider](http://code.angularjs.org/1.1.0/docs/api/ng.$routeProvider)來聲明,它是[$route](http://code.angularjs.org/1.1.0/docs/api/ng.$route)服務的提供者。這項服務使得控制器、視圖模板與當前瀏覽器的URL可以輕易集成。應用這個特性我們就可以實現[深鏈接](http://en.wikipedia.org/wiki/Deep_linking),它允許我們使用瀏覽器的歷史(回退或者前進導航)和書簽。
## 關于依賴注入(DI),注入器(Injector)和服務提供者(Providers)
正如從前面你學到的,[依賴注入](http://code.angularjs.org/1.1.0/docs/guide/di)是AngularJS的核心特性,所以你必須要知道一點這家伙是怎么工作的。
當應用引導時,AngularJS會創建一個注入器,我們應用后面所有依賴注入的服務都會需要它。這個注入器自己并不知道`$http`和`$route`是干什么的,實際上除非它在模塊定義的時候被配置過,否則它根本都不知道這些服務的存在。注入器唯一的職責是載入指定的服務模塊,在這些模塊中注冊所有定義的服務提供者,并且當需要時給一個指定的函數注入依賴(服務)。這些依賴通過它們的提供者“懶惰式”(需要時才加載)實例化。
提供者是提供(創建)服務實例并且對外提供API接口的對象,它可以被用來控制一個服務的創建和運行時行為。對于`$route`服務來說,`$routeProvider`對外提供了API接口,通過API接口允許你為你的應用定義路由規則。
AngularJS模塊解決了從應用中刪除全局狀態和提供方法來配置注入器這兩個問題。和`AMD`或者`require.js`這兩個模塊(非AngularJS的兩個庫)不同的是,AngularJS模塊并沒有試圖去解決腳本加載順序以及懶惰式腳本加載這樣的問題。這些目標和AngularJS要解決的問題毫無關聯,所以這些模塊完全可以共存來實現各自的目標。
## App 模塊
app/js/app.js
~~~
angular.module('phonecat', []).
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'});
}]);
~~~
為了給我們的應用配置路由,我們需要給應用創建一個模塊。我們管這個模塊叫做`phonecat`,并且通過使用`config`API,我們請求把`$routeProvider`注入到我們的配置函數并且使用`$routeProvider.when`API來定義我們的路由規則。
注意到在注入器配置階段,提供者也可以同時被注入,但是一旦注入器被創建并且開始創建服務實例的時候,他們就不再會被外界所獲取到。
我們的路由規則定義如下
* 當URL 映射段為`/phones`時,手機列表視圖會被顯示出來。為了構造這個視圖,AngularJS會使用`phone-list.html`模板和`PhoneListCtrl`控制器。
* 當URL 映射段為`/phone/:phoneId`時,手機詳細信息視圖被顯示出來。這里`:phoneId`是URL的變量部分。為了構造手機詳細視圖,AngularJS會使用`phone-detail.html`模板和`PhoneDetailCtrl`控制器。
我們重用之前創造過的`PhoneListCtrl`控制器,同時我們為手機詳細視圖添加一個新的`PhoneDetailCtrl`控制器,把它存放在`app/js/controllers.js`文件里。
`$route.otherwise({redirectTo: '/phones'})`語句使得當瀏覽器地址不能匹配我們任何一個路由規則時,觸發重定向到`/phones`。
注意到在第二條路由聲明中`:phoneId`參數的使用。`$route`服務使用路由聲明`/phones/:phoneId`作為一個匹配當前URL的模板。所有以`:`符號聲明的變量(此處變量為`phones`)都會被提取,然后存放在[$routeParams](http://code.angularjs.org/1.1.0/docs/api/ng.$routeParams)對象中。
為了讓我們的應用引導我們新創建的模塊,我們同時需要在[ngApp](http://code.angularjs.org/1.1.0/docs/api/ng.directive:ngApp)指令的值上指明模塊的名字:
app/index.html
~~~
<!doctype html>
<html lang="en" ng-app="phonecat">
...
~~~
## 控制器
app/js/controllers.js
~~~
...
function PhoneDetailCtrl($scope, $routeParams) {
$scope.phoneId = $routeParams.phoneId;
}
//PhoneDetailCtrl.$inject = ['$scope', '$routeParams'];
~~~
## 模板
`$route`服務通常和[ngView](http://code.angularjs.org/1.1.0/docs/api/ng.directive:ngView)指令一起使用。`ngView`指令的角色是為當前路由把對應的視圖模板載入到布局模板中。
app/index.html
~~~
<html lang="en" ng-app="phonecat">
<head>
...
<script src="lib/angular/angular.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
</head>
<body>
<div ng-view></div>
</body>
</html>
~~~
注意,我們把`index.html`模板里面大部分代碼移除,我們只放置了一個`<div>`容器,這個`<div>`具有`ng-view`屬性。我們刪除掉的代碼現在被放置在`phone-list.html`模板中:
app/partials/phone-list.html
~~~
<div class="container-fluid">
<div class="row-fluid">
<div class="span2">
<!--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="span10">
<!--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 {{phoneId}}
~~~
注意到我們的布局模板中沒再添加`PhoneListCtrl`或`PhoneDetailCtrl`控制器屬性!
## 測試
為了自動驗證所有的東西都良好地集成起來,我們需要寫一些端到端測試,導航到不同的URL上然后驗證正確地視圖被渲染出來。
~~~
...
it('should redirect index.html to index.html#/phones', function() {
browser().navigateTo('../../app/index.html');
expect(browser().location().url()).toBe('/phones');
});
...
describe('Phone detail view', function() {
beforeEach(function() {
browser().navigateTo('../../app/index.html#/phones/nexus-s');
});
it('should display placeholder page with phoneId', function() {
expect(binding('phoneId')).toBe('nexus-s');
});
});
~~~
你現在可以刷新你的瀏覽器,然后重新跑一遍端到端測試,或者你可以在[AngularJS的服務器](http://angular.github.com/angular-phonecat/step-4/test/e2e/runner.html)上運行一下。
## 練習
試著在`index.html`上增加一個`{{orderProp}}`綁定,當你在手機列表視圖上時什么也沒變。這是因為`orderProp`模型僅僅在`PhoneListCtrl`管理的作用域下才是可見的,這與`<div ng-view>`元素相關。如果你在`phone-list.html`模板中加入同樣的綁定,那么這個綁定會按你設想的那樣被渲染出來。
## 總結
設置路由并實現手機列表視圖之后,我們已經可以進入[步驟8](a00b)來實現手機詳細信息視圖了。