到現在為止,我們使用是硬編碼的三條手機記錄數據集。現在我們使用AngularJS一個內置服務[$http](http://code.angularjs.org/1.1.0/docs/api/ng.$http)來獲取一個更大的手機記錄數據集。我們將使用AngularJS的[依賴注入(dependency injection (DI))](http://angularjs.cn/A00z)功能來為`PhoneListCtrl`控制器提供這個AngularJS服務。
請重置工作目錄:
~~~
git checkout -f step-5
~~~
刷新瀏覽器,你現在應該能看到一個20部手機的列表。
步驟4和步驟5之間最重要的不同在下面列出。你可以在[GitHub](https://github.com/angular/angular-phonecat/compare/step-4...step-5)里看到完整的差別。
## 數據
你項目當中的`app/phones/phones.json`文件是一個數據集,它以JSON格式存儲了一張更大的手機列表。
下面是這個文件的一個樣例:
~~~
[
{
"age": 13,
"id": "motorola-defy-with-motoblur",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"snippet": "Are you ready for everything life throws your way?"
...
},
...
]
~~~
## 控制器
我們在控制器中使用AngularJS服務[$http](http://code.angularjs.org/1.1.0/docs/api/ng.$http)向你的Web服務器發起一個HTTP請求,以此從`app/phones/phones.json`文件中獲取數據。`$http`僅僅是AngularJS眾多內建服務中之一,這些服務可以處理一些Web應用的通用操作。AngularJS能將這些服務注入到任何你需要它們的地方。
服務是通過AngularJS的[依賴注入DI子系統](http://angularjs.cn/A00z)來管理的。依賴注入服務可以使你的Web應用良好構建(比如分離表現層、數據和控制三者的部件)并且松耦合(一個部件自己不需要解決部件之間的依賴問題,它們都被DI子系統所處理)。
app/js/controllers.js
~~~
function PhoneListCtrl($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}
//PhoneListCtrl.$inject = ['$scope', '$http'];
~~~
`$http`向Web服務器發起一個`HTTP GET`請求,索取`phone/phones.json`(注意,url是相對于我們的`index.html`文件的)。服務器用json文件中的數據作為響應。(這個響應或許是實時從后端服務器動態產生的。但是對于瀏覽器來說,它們看起來都是一樣的。為了簡單起見,我們在教程里面簡單地使用了一個json文件。)
`$http`服務用`success`返回[對象應答][ng.$q]。當異步響應到達時,用這個對象應答函數來處理服務器響應的數據,并且把數據賦值給作用域的`phones`數據模型。注意到AngularJS會自動檢測到這個json應答,并且已經為我們解析出來了!
為了使用AngularJS的服務,你只需要在控制器的構造函數里面作為參數聲明出所需服務的名字,就像這樣:
~~~
function PhoneListCtrl($scope, $http) {...}
~~~
當控制器構造的時候,AngularJS的依賴注入器會將這些服務注入到你的控制器中。當然,依賴注入器也會處理所需服務可能存在的任何傳遞性依賴(一個服務通常會依賴于其他的服務)。
注意到參數名字非常重要,因為注入器會用他們去尋找相應的依賴。
### '$'前綴命名習慣
你可以創建自己的服務,實際上我們在[步驟11](http://www.angularjs.cn/docs/tutorial/488.html)就會學習到它。作為一個命名習慣,AngularJS內建服務,作用域方法,以及一些其他的AngularJS API都在名字前面使用一個‘$’前綴。不要使用‘$’前綴來命名你自己的服務和模型,否則可能會產生名字沖突。
### 關于JS壓縮
由于AngularJS是通過控制器構造函數的參數名字來推斷依賴服務名稱的。所以如果你要[壓縮](http://en.wikipedia.org/wiki/Minification_(programming))`PhoneListCtrl`控制器的JS代碼,它所有的參數也同時會被壓縮,這時候依賴注入系統就不能正確的識別出服務了。
為了克服壓縮引起的問題,只要在控制器函數里面給`$inject`屬性賦值一個依賴服務標識符的數組,就像被注釋掉那段最后一行那樣:
~~~
PhoneListCtrl.$inject = ['$scope', '$http'];
~~~
另一種方法也可以用來指定依賴列表并且避免壓縮問題——使用Javascript數組方式構造控制器:把要注入的服務放到一個字符串數組(代表依賴的名字)里,數組最后一個元素是控制器的方法函數:
~~~
var PhoneListCtrl = ['$scope', '$http', function($scope, $http) { /* constructor body */ }];
~~~
上面提到的兩種方法都能和AngularJS可注入的任何函數完美協作,要選哪一種方式完全取決于你們項目的編程風格,建議使用數組方式。
## 測試
test/unit/controllerSpec.js:
由于我們現在開始使用依賴注入,并且我們的控制器也含有了許多依賴服務,所以為我們的控制器構造測試就有一點小小的復雜了。我們需要使用`new`操作并且提供給構造器包括`$http`的一些偽實現。然而,我們推薦的方法(而且更加簡單噢)是在測試環境下創建一個控制器,使用的方法和AngularJS在產品代碼于下面的場景下做的一樣:
~~~
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
var scope, ctrl, $httpBackend;
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/phones.json').
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
scope = $rootScope.$new();
ctrl = $controller(PhoneListCtrl, {$scope: scope});
}));
~~~
注意:因為我們在測試環境中加載了Jasmine和angular-mock.js,我們有了兩個輔助方法,[module](http://code.angularjs.org/1.1.0/docs/api/angular.mock.module)和[inject](http://code.angularjs.org/1.1.0/docs/api/angular.mock.inject),來幫助我們獲得和配置注入器。
用如下方法,我們在測試環境中創建一個控制器:
* 我們使用`inject`方法將[$rootScope](http://code.angularjs.org/1.1.0/docs/api/ng.$rootScope),[$controller](http://code.angularjs.org/1.1.0/docs/api/ng.$controller)和[$httpBackend](http://code.angularjs.org/1.1.0/docs/api/ng.$httpBackend)服務實例注入到Jasmine的`beforeEach`函數里。這些實例都來自一個注入器,但是這個注入器在每一個測試內部都會被重新創建。這樣保證了每一個測試都從一個周知的起始點開始,并且每一個測試都和其他測試相互獨立。
* 調用`$rootScope.$new()`來為我們的控制器創建一個新的作用域。
* `PhoneListCtrl`函數和剛創建的作用域作為參數,傳遞給已注入的`$controller`函數。
由于我們現在的代碼在創建`PhoneListCtrl`子作用域之前,于控制器中使用`$http`服務獲取了手機列表數據,我們需要告訴測試套件等待一個從控制器來的請求。我們可以這樣做:
* 將請求服務`$httpBackend`注入到我們的`beforeEach`函數中。這是這個服務的一個偽版本,這樣做在產品環境中有助于處理所有的XHR和JSONP請求。服務的偽版本允許你不用考慮原生API和全局狀態——隨便一個都能構成測試的噩夢——就可以寫測試。
* 使用`$httpBackend.expectGET`方法來告訴`$httpBackend`服務來等待一個HTTP請求,并且告訴它如何對其進行響應。注意到,當我們調用`$httpBackend.flush`方法之前,響應是不會被發出的。
現在,
~~~
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(scope.phones).toBeUndefined();
$httpBackend.flush();
expect(scope.phones).toEqual([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
});
~~~
* 在瀏覽器里,我們調用`$httpBackend.flush()`來清空(flush)請求隊列。這樣會使得`$http`服務返回的promise(什么是promise請參見[這里](http://stackoverflow.com/questions/11004273/what-is-stdpromise))能夠被解釋成規范的應答。
* 我們設置一些斷言,來驗證手機數據模型已經在作用域里了。
最終,我們驗證`orderProp`的默認值被正確設置:
~~~
it('should set the default value of orderProp model', function() {
expect(scope.orderProp).toBe('age');
});
;
~~~
執行`./scripts/test.sh`腳本來運行測試,你應該會看到如下輸出:
~~~
Chrome: Runner reset.
..
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
Chrome 19.0.1084.36 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
~~~
## 練習
* 在index.html末尾添加一個`{{phones | json}}`綁定,觀察json格式的手機列表。
* 在`PhoneListCtrl`控制器中,把HTTP應答預處理一下,使得只顯示手機列表的前五個。在`$http`回調函數里面使用如下代碼:
~~~
$scope.phones = data.splice(0, 5);
~~~
## 總結
現在你應該感覺得到使用AngularJS的服務是多么的容易(這都要歸功于AngularJS服務的依賴注入機制),轉到[步驟6](a009),你會為手機添加縮略圖和鏈接。