是時候給這些網頁來點動態特性了——用AngularJS!咱們這裏爲後面要加入的控制器添加了一個測試。
一個應用的代碼架構有不少種。對於AngularJS應用,咱們鼓勵使用模型-視圖-控制器(MVC)模式解耦代碼和分離關注點。考慮到這一點,咱們用AngularJS來爲咱們的應用添加一些模型、視圖和控制器。
請重置工做目錄:
git checkout -f step-2
咱們的應用如今有了一個包含三部手機的列表。
步驟1和步驟2之間最重要的不一樣在下面列出。,你能夠到GitHub去看完整的差異。
視圖和模板
在AngularJS中,一個視圖是模型經過HTML**模板**渲染以後的映射。這意味着,不論模型何時發生變化,AngularJS會實時更新結合點,隨之更新視圖。
好比,視圖組件被AngularJS用下面這個模板構建出來:
<html ng-app> <head> ... <script src="lib/angular/angular.js"></script> <script src="js/controllers.js"></script> </head> <body ng-controller="PhoneListCtrl"> <ul> <li ng-repeat="phone in phones"> {{phone.name}} <p>{{phone.snippet}}</p> </li> </ul> </body> </html>
咱們剛剛把靜態編碼的手機列表替換掉了,由於這裏咱們使用ngRepeat指令和兩個用花括號包裹起來的AngularJS表達式——{{phone.name}}
和{{phone.snippet}}
——能達到一樣的效果。
- 在
<li>
標籤裏面的ng-repeat="phone in phones"
語句是一個AngularJS迭代器。這個迭代器告訴AngularJS用第一個<li>
標籤做爲模板爲列表中的每一部手機建立一個<li>
元素。
- 正如咱們在第0步時學到的,包裹在
phone.name
和phone.snippet
周圍的花括號標識着數據綁定。和常量計算不一樣的是,這裏的表達式其實是咱們應用的一個數據模型引用,這些咱們在PhoneListCtrl
控制器裏面都設置好了。
模型和控制器
在PhoneListCtrl
控制器裏面初始化了數據模型(這裏只不過是一個包含了數組的函數,數組中存儲的對象是手機數據列表):
app/js/controller.js:
function PhoneListCtrl($scope) { $scope.phones = [ {"name": "Nexus S", "snippet": "Fast just got faster with Nexus S."}, {"name": "Motorola XOOM™ with Wi-Fi", "snippet": "The Next, Next Generation tablet."}, {"name": "MOTOROLA XOOM™", "snippet": "The Next, Next Generation tablet."} ]; }
儘管控制器看起來並無起到什麼控制的做用,可是它在這裏起到了相當重要的做用。經過給定咱們數據模型的語境,控制器容許咱們創建模型和視圖之間的數據綁定。咱們是這樣把表現層,數據和邏輯部件聯繫在一塊兒的:
PhoneListCtrl
——控制器方法的名字(在JS文件controllers.js
中)和<body>
標籤裏面的ngController指令的值相匹配。
- 手機的數據此時與注入到咱們控制器函數的做用域(
$scope
)相關聯。當應用啓動以後,會有一個根做用域被建立出來,而控制器的做用域是根做用域的一個典型後繼。這個控制器的做用域對全部<body ng-controller="PhoneListCtrl">
標記內部的數據綁定有效。
AngularJS的做用域理論很是重要:一個做用域能夠視做模板、模型和控制器協同工做的粘接器。AngularJS使用做用域,同時還有模板中的信息,數據模型和控制器。這些能夠幫助模型和視圖分離,可是他們二者確實是同步的!任何對於模型的更改都會即時反映在視圖上;任何在視圖上的更改都會被馬上體如今模型中。
想要更加深刻理解AngularJS的做用域,請參看AngularJS做用域文檔。
測試
「AngularJS方式」讓開發時代碼測試變得十分簡單。讓咱們來瞅一眼下面這個爲控制器新添加的單元測試:
test/unit/controllersSpec.js:
describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ it('should create "phones" model with 3 phones', function() { var scope = {}, ctrl = new PhoneListCtrl(scope); expect(scope.phones.length).toBe(3); }); }); });
這個測試驗證了咱們的手機數組裏面有三條記錄(暫時無需弄明白這個測試腳本)。這個例子顯示出爲AngularJS的代碼建立一個單元測試是多麼的容易。正由於測試在軟件開發中是必不可少的環節,因此咱們使得在AngularJS能夠輕易地構建測試,來鼓勵開發者多寫它們。
在寫測試的時候,AngularJS的開發者傾向於使用Jasmine行爲驅動開發(BBD)框架中的語法。儘管AngularJS沒有強迫你使用Jasmine,可是咱們在教程裏面全部的測試都使用Jasmine編寫。你能夠在Jasmine的官方主頁或者Jasmine Wiki上得到相關知識。
基於AngularJS的項目被預先配置爲使用JsTestDriver來運行單元測試。你能夠像下面這樣運行測試:
- 在一個單獨的終端上,進入到
angular-phonecat
目錄而且運行./scripts/test-server.sh
來啓動測試(Windows命令行下請輸入.\scripts\test-server.bat
來運行腳本,後面腳本命令運行方式相似);
- 打開一個新的瀏覽器窗口,而且轉到http://localhost:9876 ;
選擇「Capture this browser in strict mode」。
這個時候,你能夠拋開你的窗口無論而後把這事忘了。JsTestDriver會本身把測試跑完而且把結果輸出在你的終端裏。
運行./scripts/test.sh
進行測試 。
你應當看到相似於以下的結果:
Chrome: Runner reset. . Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (2.00 ms) Chrome 19.0.1084.36 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (2.00 ms)
耶!測試經過了!或者沒有... 注意:若是在你運行測試以後發生了錯誤,關閉瀏覽器而後回到終端關了腳本,而後在從新來一邊上面的步驟。
練習
爲index.html
添加另外一個數據綁定。例如:
<p>Total number of phones: {{phones.length}}</p>
建立一個新的數據模型屬性,而且把它綁定到模板上。例如:
$scope.hello = "Hello, World!"
更新你的瀏覽器,確保顯示出來「Hello, World!」
用一個迭代器建立一個簡單的表:
<table> <tr><th>row number</th></tr> <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr> </table>
如今讓數據模型表達式的i
增長1:
<table> <tr><th>row number</th></tr> <tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr> </table>
肯定把toBe(3)
改爲toBe(4)
以後單元測試失敗,而後從新跑一遍./scripts/test.sh
腳本
迭代器過濾
咱們在上一步作了不少基礎性的訓練,因此如今咱們能夠來作一些簡單的事情嘍。咱們要加入全文檢索功能(沒錯,這個真的很是簡單!)。同時,咱們也會寫一個端到端測試,由於一個好的端到端測試能夠幫上很大忙。它監視着你的應用,而且在發生迴歸的時候迅速報告。
請重置工做目錄:
git checkout -f step-3
咱們的應用如今有了一個搜索框。注意到頁面上的手機列表隨着用戶在搜索框中的輸入而變化。
步驟2和步驟3之間最重要的不一樣在下面列出。你能夠在GitHub裏看到完整的差異。
控制器
咱們對控制器不作任何修改。
模板
app/index.html
<div class="container-fluid"> <div class="row-fluid"> <div class="span2"> <!--Sidebar content--> Search: <input ng-model="query"> </div> <div class="span10"> <!--Body content--> <ul class="phones"> <li ng-repeat="phone in phones | filter:query"> {{phone.name}} <p>{{phone.snippet}}</p> </li> </ul> </div> </div> </div>
咱們如今添加了一個<input>
標籤,而且使用AngularJS的$filter函數來處理ngRepeat指令的輸入。
這樣容許用戶輸入一個搜索條件,馬上就能看到對電話列表的搜索結果。咱們來解釋一下新的代碼:
數據綁定: 這是AngularJS的一個核心特性。當頁面加載的時候,AngularJS會根據輸入框的屬性值名字,將其與數據模型中相同名字的變量綁定在一塊兒,以確保二者的同步性。
在這段代碼中,用戶在輸入框中輸入的數據名字稱做query
,會馬上做爲列表迭代器(phone in phones | filter:
query`)其過濾器的輸入。當數據模型引發迭代器輸入變化的時候,迭代器能夠高效得更新DOM將數據模型最新的狀態反映出來。
測試
在步驟2,咱們學習了編寫和運行一個測試的方法。單元測試用來測試咱們用js編寫的控制器和其餘組件都很是方便,可是不能方便的對DOM操做和應用集成進行測試。對於這些來講,端到端測試是一個更好的選擇。
搜索特性是徹底經過模板和數據綁定實現的,因此咱們的第一個端到端測試就來驗證這些特性是否符合咱們的預期。
test/e2e/scenarios.js:
describe('PhoneCat App', function() { describe('Phone list view', function() { beforeEach(function() { browser().navigateTo('../../app/index.html'); }); it('should filter the phone list as user types into the search box', function() { expect(repeater('.phones li').count()).toBe(3); input('query').enter('nexus'); expect(repeater('.phones li').count()).toBe(1); input('query').enter('motorola'); expect(repeater('.phones li').count()).toBe(2); }); }); });
儘管這段測試代碼的語法看起來和咱們以前用Jasmine寫的單元測試很是像,可是端到端測試使用的是AngularJS端到端測試器提供的接口。
運行一個端到端測試,在瀏覽器新標籤頁中打開下面任意一個:
這個測試驗證了搜素框和迭代器被正確地集成起來。你能夠發現,在AngularJS裏寫一個端到端測試多麼的簡單。儘管這個例子僅僅是一個簡單的測試,可是用它來構建任何一個複雜、可讀的端到端測試都很容易。
練習
- 在
index.html
模板中添加一個{{query}}
綁定來實時顯示query
模型的當前值,而後觀察他們是如何根據輸入框中的值而變化。
如今咱們來看一下咱們怎麼讓query
模型的值出如今HTML的頁面標題上。
你或許認爲像下面這樣在title
標籤上加上一個綁定就好了:
<title>Google Phone Gallery: {{query}}</title>
可是,當你重載頁面的時候,你根本沒辦法獲得指望的結果。這是由於query
模型僅僅在body
元素定義的做用域內纔有效。
<body ng-controller="PhoneListCtrl">
若是你想讓<title>
元素綁定上query
模型,你必須把ngController
聲明移動到HTML
元素上,由於它是title
和body
元素的共同祖先。
<html ng-app ng-controller="PhoneListCtrl">
必定要注意把body
元素上的ng-controller
聲明給刪了。
當綁定兩個花括號在title
元素上能夠實現咱們的目標,可是你或許發現了,頁面正加載的時候它們已經顯示給用戶看了。一個更好的解決方案是使用ngBind或者ngBindTemplate指令,它們在頁面加載時對用戶是不可見的:
<title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
在test/e2e/scenarios.js
的describe
塊中加入下面這些端到端測試代碼:
it('should display the current filter value within an element with id "status"', function() { expect(element('#status').text()).toMatch(/Current filter: \s*$/); input('query').enter('nexus'); expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/); //alternative version of the last assertion that tests just the value of the binding using('#status').expect(binding('query')).toBe('nexus'); });
刷新瀏覽器,端到端測試器會報告測試失敗。爲了讓測試經過,編輯index.html
,添加一個id爲「status」
的div
或者p
元素,內容是一個query
綁定,再加上Current filter:
前綴。例如:
<div id="status">Current filter: {{query}}</div>
在端到端測試裏面加一條pause();
語句,從新跑一遍。你將發現測試器暫停了!這樣容許你有機會在測試運行過程當中查看你應用的狀態。測試應用是實時的!你能夠更換搜索內容來證實。稍有經驗你就會知道,這對於在端到端測試中迅速找到問題是多麼的關鍵。
雙向綁定
在這一步你會增長一個讓用戶控制手機列表顯示順序的特性。動態排序能夠這樣實現,添加一個新的模型屬性,把它和迭代器集成起來,而後讓數據綁定完成剩下的事情。
請重置工做目錄:
git checkout -f step-4
你應該發現除了搜索框以外,你的應用多了一個下來菜單,它能夠容許控制電話排列的順序。
步驟3和步驟4之間最重要的不一樣在下面列出。你能夠在GitHub裏看到完整的差異。
模板
app/index.html
Search: <input ng-model="query"> Sort by: <select ng-model="orderProp"> <option value="name">Alphabetical</option> <option value="age">Newest</option> </select> <ul class="phones"> <li ng-repeat="phone in phones | filter:query | orderBy:orderProp"> {{phone.name}} <p>{{phone.snippet}}</p> </li> </ul>
咱們在index.html中作了以下更改:
- 首先,咱們增長了一個叫作
orderProp
的<select>
標籤,這樣咱們的用戶就能夠選擇咱們提供的兩種排序方法。
- 而後,在
filter
過濾器後面添加一個orderBy過濾器用其來處理進入迭代器的數據。orderBy
過濾器以一個數組做爲輸入,複製一份副本,而後把副本重排序再輸出到迭代器。
AngularJS在select
元素和orderProp
模型之間建立了一個雙向綁定。然後,orderProp
會被用做orderBy
過濾器的輸入。
正如咱們在步驟3中討論數據綁定和迭代器的時候所說的同樣,不管何時數據模型發生了改變(好比用戶在下拉菜單中選了不一樣的順序),AngularJS的數據綁定會讓視圖自動更新。沒有任何笨拙的DOM操做!
控制器
app/js/controllers.js:
function PhoneListCtrl($scope) { $scope.phones = [ {"name": "Nexus S", "snippet": "Fast just got faster with Nexus S.", "age": 0}, {"name": "Motorola XOOM™ with Wi-Fi", "snippet": "The Next, Next Generation tablet.", "age": 1}, {"name": "MOTOROLA XOOM™", "snippet": "The Next, Next Generation tablet.", "age": 2} ]; $scope.orderProp = 'age'; }
- 咱們修改了
phones
模型—— 手機的數組 ——爲每個手機記錄其增長了一個age
屬性。咱們會根據age
屬性來對手機進行排序。
咱們在控制器代碼里加了一行讓orderProp
的默認值爲age
。若是咱們不設置默認值,這個模型會在咱們的用戶在下拉菜單選擇一個順序以前一直處於未初始化狀態。
如今咱們該好好談談雙向數據綁定了。注意到當應用在瀏覽器中加載時,「Newest」在下拉菜單中被選中。這是由於咱們在控制器中把orderProp
設置成了‘age’。因此綁定在從咱們模型到用戶界面的方向上起做用——即數據從模型到視圖的綁定。如今當你在下拉菜單中選擇「Alphabetically」,數據模型會被同時更新,而且手機列表數組會被從新排序。這個時候數據綁定從另外一個方向產生了做用——即數據從視圖到模型的綁定。
測試