- AngularJS深度剖析與最佳實踐
- 雪狼 破狼 彭洪偉
- 189字
- 2019-01-01 01:21:41
第2章 概念介紹
子曰:必也正名乎。在實際開發(fā)中,讓每個人對概念都有一致的理解是很重要的,寫書也是如此。
在上一章,我們使用了很多概念,并且假設(shè)你已經(jīng)知道了這些概念的名字和大致用法。對于入門來說,這樣已經(jīng)足夠了,但本書可不是一本入門書籍。
本章中,我們將深入講解這些概念,有些提法甚至可能顛覆你以前的認(rèn)知。不過沒關(guān)系,這是成長中的重要一步。
有了穩(wěn)固的基礎(chǔ),才能理解后面更深的專家級章節(jié)。
2.1 什么是UI
提起UI,你一定知道它是指用戶界面(User Interface),但是如果細(xì)細(xì)剖析,你會發(fā)現(xiàn)它沒那么簡單。
對于一個用戶界面,它實際上包括三個主要部分:
? 內(nèi)容:你想展現(xiàn)哪些信息?包括動態(tài)信息和靜態(tài)信息。注意,這里的內(nèi)容不包括它的格式,比如生日,跟它顯示為紅色還是綠色無關(guān),跟它顯示為年月日還是顯示為生辰八字也無關(guān)。
? 外觀:這些信息要展示為什么樣子?這包括格式和樣式。樣式還包括靜態(tài)樣式和動畫效果等。
? 交互:用戶點擊了加入購物車按鈕時會發(fā)生什么?還要更新哪些顯示?
在前端技術(shù)棧中,這三個部分分別由三項技術(shù)來負(fù)責(zé):HTML負(fù)責(zé)描述內(nèi)容,CSS負(fù)責(zé)描述外觀,JavaScript負(fù)責(zé)實現(xiàn)交互。當(dāng)然,這三者之間沒有明確的界限,比如有些格式化需要JavaScript來實現(xiàn),而HTML也往往會影響一些樣式。
如果進(jìn)一步抽象,它們分別對應(yīng)MVC的三個主要部分:內(nèi)容—Model,外觀—View,交互—Controller。
對應(yīng)到Angular中的概念,“靜態(tài)內(nèi)容”對應(yīng)模板,“動態(tài)內(nèi)容”對應(yīng)Scope,交互對應(yīng)Controller,外觀部分略微復(fù)雜點:CSS決定樣式,過濾器(filter)則決定格式。
有了這些概念為基礎(chǔ),我們再來深入講解下Angular中的概念。
2.2 模塊
與其他現(xiàn)代語言不同,當(dāng)前版本的JavaScript(ECMAScript 5)并沒有內(nèi)置模塊化語法。但是,隨著程序規(guī)模越來越大,模塊化的需求越來越重要,于是出現(xiàn)了require.js等第三方庫,試圖用庫來彌補(bǔ)語言的不足。Angular并不依賴require.js等第三方庫,而是自己實現(xiàn)了模塊化系統(tǒng),這個系統(tǒng)的核心就是模塊(module)。
我們先來回顧一下“模塊”的概念,然后自然就明白Angular中的module是怎么回事了。
所謂模塊是指把相關(guān)的一組編程元素(如類、函數(shù)、變量等)組織到同一個發(fā)布包中。這些編程元素之間緊密協(xié)作,隱藏實現(xiàn)細(xì)節(jié),只通過公開的接口與其他模塊合作。
模塊是一個粒度適中的復(fù)用單位,也是最常見的復(fù)用形式。比如我們常用的第三方庫往往對外公開好幾個類,使用者只要關(guān)注其公開接口就可以了,不用了解其實現(xiàn)細(xì)節(jié),這種第三方庫就是一個“模塊”。
Angular的module也是如此。Angular中的編程元素包括Service、Directive、Filter、Controller等,它們只有通過模塊進(jìn)行“導(dǎo)出”才能供別人使用。如:angular.module('com. ngnice.app').service('ui', function() {...});語句的作用是:先取出一個名為com.ngnice.app的模塊,然后把function() {...}作為一個回調(diào)函數(shù)以ui作為名字注冊進(jìn)去。這樣,別人就可以隨時通過ui這個名字把它從com.ngnice.app模塊中取出來。
所以,我們可以簡單地把模塊看做一個注冊表(registry),它保存著名字和編程元素的對照表,既可以存入,也可以取出。
一個程序往往不會只含有一個模塊,這些模塊需要互相協(xié)作,這就導(dǎo)致了模塊之間具有依賴關(guān)系,比如有一個可復(fù)用模塊,名叫common,而我們的應(yīng)用想要使用其中名叫authHandler的service。那么我們就要先聲明這種依賴關(guān)系:angular.module('com.ngnice. app', ['common']),這樣,Angular就知道該去common模塊中查找這個名叫authHandler的Service。如果沒有聲明這種依賴關(guān)系,那么就算引入了它所在的JavaScript文件,也照樣是無法找到的,這是新手很容易踩坑的地方,請?zhí)貏e注意。同時,Angular還可以自動檢測出循環(huán)依賴,以免出現(xiàn)無限遞歸。
注意,我們剛才調(diào)用了兩次module函數(shù):angular.module('com.ngnice.app')和angular. module('com.ngnice.app', ['common']),前者可不是后者的簡寫形式,而是具有完全不同的含義:前者的作用是引用一個模塊,也就是說查找一個名叫app的模塊,并返回其引用,如果模塊不存在,則會觸發(fā)一個異常[$injector:nomod] Module 'com.ngnice.app' is not available...;而后者的作用是創(chuàng)建一個模塊,并且聲明這個模塊需要依賴一個名為common的模塊,第二個參數(shù)是個數(shù)組,所以還可以聲明依賴更多個模塊。
模塊依賴關(guān)系是一棵樹,這就意味著:凡是依賴了app模塊的更高級模塊,也會自動依賴app所依賴的common等模塊。
2.3 作用域
如果你曾經(jīng)用jQuery寫過傳統(tǒng)的非MVC前端程序,那么在第1章“從實戰(zhàn)開始”中看到的這些代碼可能會讓你感到驚訝—不再有連篇累牘的DOM操作,不再有混合了業(yè)務(wù)邏輯和視圖邏輯的臃腫代碼,不再一聽到寫測試就閃過“不可能”三個字……
“光榮屬于MVC!”我們說Angular是個MVW(Model-View-Whatever)風(fēng)格的框架,而在Angular中扮演Model角色的概念,就是作用域(scope)。
在Angular中,scope通過原型繼承的方式被組織成了一棵樹,它的根節(jié)點就是$rootScope,這是Angular在啟動時自動創(chuàng)建的,它通常對應(yīng)于ng-app指令,并且關(guān)聯(lián)到ng-app所在的節(jié)點。
接下來,Angular會解析ng-app包含的HTML,其中的一些指令,如ng-view、ng-if、ng-repeat等也會創(chuàng)建自己的scope,這些scope都是從$rootScope及其子scope創(chuàng)建出來的,它們的嵌套關(guān)系也和這些指令的嵌套關(guān)系一致。
由于它們使用了原型繼承的方式,所以,凡是上級scope擁有的屬性,都可以從下級scope中讀取,但是當(dāng)需要對這些繼承下來的屬性進(jìn)行寫入的時候,問題就來了:寫入會導(dǎo)致在下級scope上創(chuàng)建一個同名屬性,而不會修改上級scope上的屬性。這不是Bug,而是“原型繼承”的固有語義,這是用慣了“類繼承”的后端程序員需要特別注意的。更詳細(xì)的分析和解決方式請參見4.9節(jié)“使用controller as vm方式”。
這種scope樹很好的體現(xiàn)了Model之間的嵌套關(guān)系,對業(yè)務(wù)數(shù)據(jù)的結(jié)構(gòu)是一個很恰當(dāng)?shù)某橄蟆?/p>
scope之所以如此重要,是因為它事實上是Angular解耦業(yè)務(wù)邏輯層和視圖層的關(guān)鍵:Controller操作scope,View則展現(xiàn)scope的內(nèi)容,傳統(tǒng)前端程序中大量復(fù)雜的DOM操縱邏輯都被轉(zhuǎn)變成對scope的操作。
這種樹形結(jié)構(gòu)不但體現(xiàn)在數(shù)據(jù)的繼承關(guān)系上,而且體現(xiàn)在消息的冒泡機(jī)制上。有DOM基礎(chǔ)的同學(xué)可以拿它和DOM的消息冒泡機(jī)制做類比,在后面的2.11節(jié)“消息”中我也會專門對此進(jìn)行講解。
2.4 控制器
和傳統(tǒng)的MVC程序中一樣,Angular中的控制器(controller)用來對模塊(scope)進(jìn)行操作,包括初始化數(shù)據(jù)和定義事件響應(yīng)函數(shù)等。
我們常見的定義控制器的方法是:angular.module('com.ngnice.app').controller ('UserListCtrl', function() {...});,其中:angular.module('com.ngnice.app')語句在前面已經(jīng)講過,是返回一個現(xiàn)有的module實例,而Controller就是這個module實例上的一個方法,它的作用是把后面的函數(shù)以UserListCtrl為名字,注冊到模塊中去,以便需要時可以根據(jù)名字找到它。
使用控制器的場景有幾種。最常見的是用在路由中。
比如在angular-ui-router中,我們可以通過下面的語句使用它:
$stateProvider.state('user.list', { url: '/list', templateUrl: 'views/user/list.html', controller: 'UserListCtrl' });
這樣,當(dāng)用戶訪問/user/list這個URL的時候,angular-ui-router插件就會實例化一個名為UserListCtrl的控制器,同時,創(chuàng)建一個scope對象并傳給它。這個控制器實例會在這個scope對象上創(chuàng)建屬性、方法等,這個過程稱為“初始化scope對象”。
初始化完之后,加載模板,并且把scope對象傳入,模板中會通過Angular指令綁定這些屬性、方法。Angular會通過一個稱為摘要循環(huán)(digest cycle)的過程,自動維護(hù)scope變量和視圖中DOM節(jié)點的一致性,這時候的DOM我們稱之為“活DOM(Live DOM)”。更具體的工作原理我們會在第3章“背后的原理”的3.2節(jié)“Angular啟動過程”中詳細(xì)講解。
另一個常用的場景是在單元測試中。
通常,在單元測試階段我們不必測試視圖,而是將其留待端到端測試階段。在單元測試階段,我們要關(guān)注的是控制器的工作邏輯。前面我們提過,控制器的作用是在scope對象上創(chuàng)建屬性、方法,所以我們的測試邏輯就是看它是否創(chuàng)建了正確的屬性,以及由它創(chuàng)建的方法是否能正常工作。
于是我們得出了下列測試邏輯:
var scope = $rootScope.$new(); var ctrl = $controller('UserListCtrl'); ctrl(scope); // TODO: 檢測scope中是否如同預(yù)期的產(chǎn)生了初始數(shù)據(jù) // TODO: 調(diào)用scope中的方法,然后檢測scope變量是否發(fā)生了預(yù)期的變化
其中的$controller是Angular提供的一個系統(tǒng)服務(wù),用來查找以前通過module. controller('UserListCtrl', function() {...});注冊的控制器函數(shù)。
還有一個不常用但值得提倡的場景是在指令中,特別是用來封裝一個界面片段的“組件型指令”,我們在2.6節(jié)“指令”中會進(jìn)一步展開講解。之所以在這種類型的指令中提倡使用控制器,主要是為了方便進(jìn)行單元測試,而不用引入對視圖層的測試。
有一些第三方服務(wù)或指令也會使用控制器,它們所做的工作實際上和angular-ui-router一樣的:創(chuàng)建一個scope,找到一個控制器,然后用控制器對scope進(jìn)行初始化,最后把scope綁定到視圖,把生成的Live DOM渲染出來。
掌握了控制器的工作原理,在自己的代碼中也可以靈活運(yùn)用,凡是需要Live DOM的地方都可以通過各種形式使用。
生成Live DOM時涉及另一個重要的知識點$compile,完整的實現(xiàn)方式我會在后面2.6節(jié)“指令”中講解。
2.5 視圖
我們有了模塊和控制器之后,內(nèi)容和交互邏輯就已經(jīng)基本確定了,接下來我們就得把它們展示給用戶了,這時就用到“視圖”(view)。
CSS并不在Angular的范圍內(nèi),在實踐中,常常結(jié)合一套成熟的CSS架構(gòu)來做,比如Bootstrap就可以和Angular結(jié)合得非常好。
Angular中實現(xiàn)視圖的主體是模板。最常見的模板形式當(dāng)然是HTML,也有通過Jade等中間語言編譯為HTML的。模板中包括靜態(tài)信息和動態(tài)信息,靜態(tài)信息是指直接寫死(hard code)在模板中的,而動態(tài)信息則是對scope中內(nèi)容的展示。
展示動態(tài)信息的方式有兩種:
? 綁定表達(dá)式:形式如{{username}},綁定表達(dá)式可以出現(xiàn)在HTML中的文本部分或節(jié)點的屬性部分。
? 指令:形式如<span ng-bind="username"></span>,事實上任何指令都可以用來展示動態(tài)信息,展示的方式取決于指令的內(nèi)在實現(xiàn)邏輯。
視圖中的綁定表達(dá)式或指令中用到的變量或函數(shù)都是$scope中的一個屬性或方法,上面兩個表達(dá)式綁定的都是$scope.username,但是也可以綁定到方法,如$scope. getFirstName()。綁定到方法是一種很常用的方式,但是如果使用不當(dāng),也可能導(dǎo)致一些很難追查的Bug。特別要注意不要在方法中返回一個新對象或新數(shù)組,否則會出現(xiàn)“10$digest iterations reached.”錯誤。
另一個要點是,在表達(dá)式中無法直接使用window對象下的全局屬性或方法。Angular這樣的設(shè)計可以確保視圖的局部性,以免受到意料之外的干擾而出現(xiàn)Bug。如果確實需要調(diào)用,請在Controller中簡單包裝一層。
除了展示信息之外,常常還需要對信息進(jìn)行格式化,比如把一個Date對象格式化為“年-月-日”格式或“年-月-日 時:分:秒”格式。一個分層清晰的系統(tǒng),在scope中通常是沒有格式的概念的,想要顯示成什么格式是視圖層的事情。這樣的設(shè)計可以提高M(jìn)odel層的內(nèi)聚性(只做一件事),把與此相關(guān)的需求變更在視圖層處理,可以提高M(jìn)odel層的穩(wěn)定性。
Angular中對信息進(jìn)行格式化的機(jī)制是過濾器(Filter),如:{{birthday|date}}。對過濾器的更深入講解,請參見稍后的2.7節(jié)“過濾器”。
2.6 指令
指令(directive)是Angular中一個很重要的概念,相當(dāng)于一個自定義的HTML元素,在Angular官方文檔中稱它為HTML語言的DSL(特定領(lǐng)域語言)擴(kuò)展。
按照指令的使用場景和作用可以分為兩種類型的指令:它們分別為組件型指令(Component)和裝飾型器指令(Decorator),它們的分類命名,并不是筆者獨創(chuàng)的新法,它是在Angular 2.x中提出的概念,筆者認(rèn)為它們也同樣可以使用于Angular 1.x。
組件型指令主要是為了將復(fù)雜而龐大的View分離,使得頁面的View具有更強(qiáng)的可讀性和維護(hù)性,實現(xiàn)“高內(nèi)聚低耦合”和“分離關(guān)注點”的有效手段;而裝飾器型指令則是為DOM添加行為,使其具有某種能力,如自動聚焦(autoFocus)、雙向綁定、可點擊(ngClick)、條件顯示/隱藏(ngShow/ngHide)等能力,同時它也是鏈接Model和View之間的橋梁,保持View和Model的同步。在Angular中內(nèi)置的大多數(shù)指令,是屬于裝飾器型指令,它們負(fù)責(zé)收集和創(chuàng)建$watch,然后利用Angular的“臟檢查機(jī)制”保持View的同步。
對于組件型指令和裝飾器型指令的這兩種區(qū)分是非常重要的,它們在寫法、業(yè)務(wù)含義、適用范圍等方面都有非常明顯的區(qū)別,理解了它們,對于我們?nèi)粘5闹噶铋_發(fā)也具有很好的指導(dǎo)作用。
2.6.1 組件型指令
組件型指令是一個小型的、自封裝和內(nèi)聚的一個整體,它包含業(yè)務(wù)所需要顯示的視圖以及交互邏輯,比如:我們需要在首頁放置一個登錄框和一個FAQ列表,如果我們把它們都直接寫在首頁的視圖和控制器中,那么首頁的視圖和控制器將會變得非常龐大,這樣不利于我們的分工協(xié)作和頁面的長期維護(hù)。這時候更好的方案應(yīng)該是,把它們拆分成兩個獨立的內(nèi)聚的指令login-panel和faq-list,然后分別將<login-panel></login-panel>和<faq-list></faq-list>兩個指令嵌入到首頁。
注意,我們在這里拆出這兩個指令的直接目的不是為了復(fù)用,更重要的目的應(yīng)該是分離View,促進(jìn)代碼結(jié)構(gòu)的優(yōu)化,達(dá)到更好的語義化和組件化,當(dāng)然對于這樣獨立內(nèi)聚的指令,有時我們還能意外地獲得更好的復(fù)用性。
組件型指令應(yīng)該是滿足封裝的自治性、內(nèi)聚性的,它不應(yīng)該直接引用當(dāng)前頁面的DOM結(jié)構(gòu)、數(shù)據(jù)等。如果存在需要的信息,則可以通過指令的屬性傳遞或者利用后端服務(wù)接口來自我滿足。如login-panel應(yīng)該在其內(nèi)部訪問登錄接口來實現(xiàn)自我的功能封裝。它的Scope應(yīng)該是獨立的(isolated),不需要對父作用域的結(jié)構(gòu)有任何依賴,否則一旦父作用域的結(jié)構(gòu)發(fā)生改變,可能它也需要相應(yīng)地變更,這種封裝是很脆弱的。更好的封裝應(yīng)該是“高內(nèi)聚低耦合”的,內(nèi)聚是描述組件內(nèi)部實現(xiàn)了它所應(yīng)該包含的邏輯功能,耦合則描述它和外部組件之間應(yīng)該是盡量少的相互依賴。
組件型指令的寫法通常是這樣的:
// 聲明一個指令 angular.module('com.ngnice.app').directive('jobCategory', function () { return { // 可以用作HTML元素,也可以用作HTML屬性 restrict: 'EA', // 使用獨立作用域 scope: { configure: '=' }, // 指定模板 templateUrl: 'components/configure/tree.html', // 聲明指令的控制器 controller: function JobCategoryCtrl($scope) { ... } }; });
指令中return的這個結(jié)果,我們稱之為“指令定義對象”。
restrict屬性用來表示這個指令的應(yīng)用方式,它的取值可以是E(元素)、A(屬性)、C(類名)、M(注釋)這幾個字母的任意組合,工程實踐中常用的是E、A、EA這三個,對于C、M筆者并不建議使用它們。對于組件型指令來說,標(biāo)準(zhǔn)的用法是E,但是為了兼容IE8,通常也支持一個A,因為IE8的自定義元素需要先用document.createElement注冊,用A可以省去注冊的麻煩。
scope有三種取值:不指定(undefined)/false、true或一個哈希對象。
不指定或為false時,表示這個指令不需要新作用域。它直接訪問現(xiàn)有作用域上的屬性或方法,也可以不訪問作用域。如果同一節(jié)點上有新作用域或獨立作用域指令,則直接使用它,否則直接使用父級作用域。
為true時,表示它需要一個新作用域,可以跟本節(jié)點上的其他新作用域指令共享作用域,如果任何指令都沒有新作用域,它就會創(chuàng)建一個。
為哈希對象時,表示它需要一個獨立的(isolated)作用域。所謂獨立作用域,是指獨立于父作用域,它不會從父節(jié)點自動繼承任何屬性,這樣的話,就不會無意間引用到父節(jié)點上的屬性,導(dǎo)致意料之外的耦合。
要注意,一個節(jié)點上如果已經(jīng)出現(xiàn)了一個獨立作用域指令,那么就不能再出現(xiàn)另一個獨立作用域指令或者新作用域指令,否則使用scope的代碼將無法區(qū)分兩者,如果自動將兩個作用域合并,又會失去“獨立性”。總之,記住一句話:獨立作用域指令是“排它”的。
那么哈希對象的內(nèi)容呢?它表示的是屬性綁定規(guī)則,如:
{ // 綁定字面量 name: '@', // 綁定變量 details: '=', // 綁定事件 onUpdate: '&' }
這里我們綁定了三個屬性,以<user-details name='test' details='details' on-update='updateIt(times)'></user-details>為例,name的值將被綁定為字符串'test',而details的值不是'details',而是綁定到父頁面scope上一個名為details的變量,當(dāng)父頁面scope的details變量變化時,指令中的值也會隨之變化—即使綁定到number等原生類型也一樣。而onUpdate綁定的則是一個回調(diào)函數(shù),它是父頁面scope上一個名為updateIt的函數(shù)。當(dāng)指令代碼中調(diào)用scope.onUpdate()的時候,父頁面scope的updateIt就會被調(diào)用。當(dāng)然,name也同樣可以綁定到變量,但是要通過綁定表達(dá)式的方式,比如<user-details name="{{name}}"></user-details>中,name將會綁定到父頁面scope中的name變量,并且也會同步更新。
記住,對于組件型指令,更重要的是內(nèi)容信息的展示,所以我們一般不涉及指令的link函數(shù),而應(yīng)該盡量地將業(yè)務(wù)邏輯放置在Controller中。
組件化的開發(fā)方式以及組件化的復(fù)用,是我們在前端開發(fā)中一直追求的一個理想目標(biāo)。從最初的iframe、jQuery UI、Extjs、jQuery easyui,我們一直在不懈地朝著組件化的方向前進(jìn)。Angular首次在其框架中提出指令這種以HTML DSL方式進(jìn)行語義化、組件化擴(kuò)展的方式,就我們在這里描述的組件型指令。筆者也更愿意將它稱為“Directive as component”(指令即組件)。只要告訴大家下面的實例代碼是一個在線支付頁面,相信大家很快就能從頁面中讀懂它業(yè)務(wù)邏輯了:
<form novalidate name="orderForm" ng-submit="processPage();"> <error-panel errors="order.errors"></error-panel> <fieldset class="field-group"> <post-address class="post-address" view-model="order.poastAddress" post-address- change="order.postAddressChange();"></post-address> </fieldset> <fieldset class="field-group"> <payment-way class="payment-way" viewmodel="order.paymentWay"></ payment-way> </fieldset> <fieldset class="field-group"> <item-list class="item-list" viewmodel="order.items"></item-list> </fieldset> ..... <div class="submit"> <button class="btn primary-btn">提交訂單</button> </div> </form>
從上面的代碼中,我們能很快地識別出此頁面包含:全局錯誤顯示、郵寄地址、在線支付方式、購買商品信息這幾個領(lǐng)域概念,然而對于它們的修改和維護(hù)也很容易,組件更加的內(nèi)聚,并且遵守單一職責(zé)原則(SRP)。這就是“Directive as component”和組件型指令的迷人之處。
繼Angular的指令之后,React也推出了以JSX模板為核心的類HTML語法擴(kuò)展,以此來實現(xiàn)組件化的開發(fā),而且它也是React中的最重要的核心概念。Google和Mozilla也在推進(jìn)Web Component技術(shù),它主要以Custom Elements、HTML Templates、Shadow DOM、HTML Imports四大技術(shù)為核心,讓我們能夠像瀏覽器開發(fā)者一樣使用HTML、CSS、JavaScript來構(gòu)建更酷、更炫、獨立的HTML節(jié)點,使得我們能夠快速的應(yīng)對越來越復(fù)雜、多樣化的用戶體驗要求,而不是繼續(xù)等待瀏覽器廠商來實現(xiàn)它們。
有興趣的讀者,可以自行閱讀更多關(guān)于Web Component的資料。可以參考:http://webcomponents.org/、Google的polymer框架:http://www.polymer-project.org/以及Mozilla的X-Tags框架:http://x-tags.org/。
2.6.2 裝飾器型指令
對于裝飾器型指令,其定義方式則如下:
angular.module('com.ngnice.app').directive('twTitle', function () { return { // 用作屬性 restrict: 'A', link: function (scope, element, attrs) { ... } }; });
裝飾器型指令主要用于添加行為和保持View和Model的同步,所以它不同于組件型指令,我們經(jīng)常需要進(jìn)行DOM操作。其restrict屬性通常為A,也就是屬性聲明方式,這種方式更符合裝飾器的語義:它并不是一個內(nèi)容的主體,而是附加行為能力的連接器。
同時,由于多個裝飾器很可能被用于同一個指令,包括獨立作用域指令,所以裝飾器型指令通常不使用新作用域或獨立作用域。如果要訪問綁定屬性,該怎么做呢?仍然看前面的例子<user-details name="test" details="details" on-update="updateIt(times)"></user-details>,假如不使用獨立作用域,我們該如何獲取這些屬性的值呢?
? 對于@型的綁定,我們可以直接通過attrs取到它:attrs.name等價于name: '@'。
? 對于=型的綁定,我們可以通過scope.$eval取到它:scope.$eval(attrs.details)等價于details: '='。
&型的綁定理解起來會稍有困難,先看代碼:scope.$eval(attrs.onUpdate, {times: 3});。
和=型綁定一樣,onUpdate屬性在本質(zhì)上是當(dāng)前scope上的一個表達(dá)式。特殊的地方在于,這個表達(dá)式是一個函數(shù),$eval發(fā)現(xiàn)它是函數(shù)時,就可以傳一個參數(shù)表(在Angular中稱之為locals)給它。onUpdate表達(dá)式中可以使用的參數(shù)名和它的參數(shù)值,都來自這個參數(shù)表。
使用的時候,我們可以在視圖中引用這個哈希對象的某個屬性作為參數(shù),比如對于剛才的定義,視圖中的on-update="updateIt(times)"所引用的times變量就來自我們剛才在callback中傳入的times屬性,而updateIt函數(shù)被調(diào)用時將會接收到它,參數(shù)值是3。
$scope.updateIt = function(times) { // 這里times的值應(yīng)該是3,但是這個times不需要跟視圖和指令中的名稱一致,它叫什么都可以。但 視圖和指令中的名稱必須一致 };
在裝飾器指令中,其實還有一種細(xì)分的分支,它完全不操縱DOM,只是對當(dāng)前scope進(jìn)行處理,如添加成員變量、函數(shù)等。代碼如下:
angular.module('com.ngnice.app').directive('twToggle', function () { return { restrict: 'A', scope: true, link: function(scope) { scope.$node = { folded: false, toggle: function() { this.folded = !this.folded; } }; } }; }); 使用的時候: <ul> <li ng-repeat="item in items" tw-toggle=""> <span ng-click="$node.toggle()">切換</span> <ul ng-if="$node.folded"> ... </ul> </li> </ul>
它的作用是在當(dāng)前元素的作用域上創(chuàng)建一個名為$node的哈希對象,這個哈希對象具有一組自定義的屬性和方法,可用來封裝交互邏輯。
也許你已經(jīng)想到了,這種類型的指令還可以進(jìn)一步改進(jìn)。如何改進(jìn)呢?
angular.module('com.ngnice.app').directive('twToggle', function () { return { restrict: 'A', scope: true, controller: function($scope) { $scope.folded = false; $scope.toggle = function() { $scope.folded = !$scope.folded; }; } }; });
它好在哪里?筆者不直接給出答案,請讀者自行分析,并嘗試?yán)斫馑@對于指令的認(rèn)識是很重要的。
2.7 過濾器
我們在第1章中使用了多個系統(tǒng)內(nèi)置的過濾器(filter),還寫了一個自定義過濾器,這里我們再系統(tǒng)化的從理論層面講一下。
過濾器標(biāo)準(zhǔn)的定義方式是:
angular.module('com.ngnice.app').filter('myFilter', function(/* 這里可以用參數(shù)進(jìn)行依 賴注入 */) { return function(input) { var result; // TODO: 把input變換成result return result; }; });
可以看出,過濾器是一個特殊的函數(shù),它返回一個函數(shù),這個函數(shù)接收的第一個參數(shù)就是被過濾的變量,如使用{{1|myFilter}}時,這個input參數(shù)的值就是1,當(dāng)這個值是個變量時,它的變化會導(dǎo)致myFilter再次被執(zhí)行。
過濾器還可以接收第二個參數(shù),乃至第N個參數(shù),如:
return function(input, arg1, arg2, arg3) { ... };
而使用者則通過{{1|myFilter:2:3:4}}的形式調(diào)用它。這種情況下,arg1的值為2,arg2的值為3,arg3的值為4。
從使用者的角度,我們可以把filter看做一個函數(shù),它負(fù)責(zé)接收輸入,然后轉(zhuǎn)換成輸出。每當(dāng)輸入?yún)?shù)發(fā)生變化時,它就被執(zhí)行,其輸出會被視圖使用。
filter除了可以用在綁定表達(dá)式之外,還可以用在指令中通過值綁定的屬性,如<li ng-repeat="item in items | filter:'a'">...</li>。
由于其簡單靈巧,filter非常適合復(fù)用。官方提供的幾個filter就有很多種用法,讀者可以參照官方的API文檔和實戰(zhàn)篇的案例,自行嘗試用ng-repeat, filter, orderBy的組合來實現(xiàn)具有前端過濾功能的表格,這有助于對過濾器的深入理解。
2.8 路由
前端“路由”(router)的概念和后端的路由是一樣的,也就是根據(jù)URL找到view-controller組合的機(jī)制。最開始的時候,Angular的路由庫合并在核心庫中,現(xiàn)在,路由庫從Angular核心庫中剝離出來。官方的路由庫稱為ngRoute,由于其過于簡陋,我在工程實踐中比較常用的是一個第三方路由庫:angular-ui-router。
ngRoute的寫法是:
$routeProvider.when('/url', { templateUrl: 'path/to/template.html', controller: 'SomeCtrl' });
angular-ui-router的寫法是:
$stateProvider.state('name', { url: '/url', templateUrl: 'path/to/template.html', controller: 'SomeCtrl' });
雖然寫法不同,但是其工作原理都是類似的:
監(jiān)聽$locationChangeSuccess事件,它將在每次URL(包括#后面的hash部分)發(fā)生變化時觸發(fā)。
在這個事件中,將根據(jù)$routeProvider/$stateProvider中注冊的路由表中的URL部分查閱其路由對象,如:
{ url: '/url', templateUrl: 'path/to/template.html',
controller: 'SomeCtrl' }
從這個路由對象中,可以取到兩個關(guān)鍵參數(shù):templateUrl/controller。
? 創(chuàng)建一個scope對象。
? 加載模板,借助瀏覽器的能力把它解析為靜態(tài)的DOM。
? 使用Controller對scope進(jìn)行初始化,添加屬性和方法。
? 使用$compile服務(wù)把剛才生成的DOM和scope關(guān)聯(lián)起來,變成一個Live DOM。
? 用這個Live DOM替換ng-view/ui-view中的所有內(nèi)容。
你可能還有印象,這個過程我們在前面的第1章也用過。
這個過程非常簡單。作為練習(xí),你可以使用類似的原理來實現(xiàn)一個簡單的路由功能,這個過程中會接觸到很多Angular核心服務(wù),對理解Angular的核心工作原理非常有用。
2.9 服務(wù)
如果你是一個后端程序員,那么對服務(wù)(Service)的概念一定不會陌生。在Angular中,服務(wù)的概念是一樣的,差別只在于技術(shù)細(xì)節(jié)。
服務(wù)是對公共代碼的抽象,比如,如果在多個控制器中都出現(xiàn)了相似的代碼,那么把它們提取出來,封裝成一個服務(wù),你將更加遵循DRY原則(即:不要重復(fù)你自己),在可維護(hù)性等方面獲得提升。
如同我們在第1章的tree服務(wù)中所看到的,由于服務(wù)剝離了和具體表現(xiàn)相關(guān)的部分,而聚焦于業(yè)務(wù)邏輯或交互邏輯,它更加容易被測試和復(fù)用。
但是,在工程實踐中,我們引入服務(wù)的首要目的是為了優(yōu)化代碼結(jié)構(gòu),而不是復(fù)用。復(fù)用只是一項結(jié)果,不是目標(biāo)。所以,當(dāng)你發(fā)現(xiàn)你的代碼中混雜了表現(xiàn)層邏輯和業(yè)務(wù)層邏輯的時候,你就要認(rèn)真考慮抽取服務(wù)了—哪怕它還看不到復(fù)用價值。
如果你遵循著測試驅(qū)動開發(fā)的方式,那么當(dāng)你覺得測試很難寫的時候,回頭審視下,看是否這里可以抽取出一個服務(wù),轉(zhuǎn)而對服務(wù)進(jìn)行測試。
服務(wù)的概念通常是和依賴注入緊密相關(guān)的,Angular中也一樣。如果你困惑于在JavaScript中是如何實現(xiàn)依賴注入的,請參見第3章“背后的原理”中的3.3節(jié)“依賴注入”。
由于依賴注入的要求,服務(wù)都是單例的,這樣我們才能把它們到處注入,而不用手動管理它們的生命周期,并容許Angular實現(xiàn)“延遲初始化”等優(yōu)化措施。
在Angular中,服務(wù)分成很多種類型:
? 常量(Constant):用于聲明不會被修改的值。
? 變量(Value):用于聲明會被修改的值。
? 服務(wù)(Service):沒錯,它跟服務(wù)這個大概念同名,原作者在“開發(fā)者指南”中把這種行為比喻為“把自己的孩子取名叫‘孩子’—一個會氣瘋老師的名字”。事實上,同名的原因是—它跟后端領(lǐng)域的“服務(wù)”實現(xiàn)方式最為相似:聲明一個類,等待Angular把它new出來,然后保存這個實例,供它到處注入。
? 工廠(Factory):它跟上面這個“服務(wù)”不同,它不會被new出來,Angular會調(diào)用這個函數(shù),獲得返回值,然后保存這個返回值,供它到處注入。它被取名為“工廠”是因為:它本身不會被用于注入,我們使用的是它的產(chǎn)品。但是與現(xiàn)實中的工廠不同,它只產(chǎn)出一份產(chǎn)品,我們只是到處使用這個產(chǎn)品而已。
? 供應(yīng)商(Provider):“工廠”只負(fù)責(zé)生產(chǎn)產(chǎn)品,它的規(guī)格是不受我們控制的,而“供應(yīng)商”更加靈活,我們可以對規(guī)格進(jìn)行配置,以便獲得定制化的產(chǎn)品。
事實上,除了Constant外,所有這些類型的服務(wù),背后都是通過Provider實現(xiàn)的,我們可以把它們看做讓Provider更容易寫的語法糖。一個明顯的佐證是:當(dāng)你使用一個未定義的服務(wù)時,Angular給你的錯誤提示是它對應(yīng)的Provider未找到,比如我們使用一個未定義的服務(wù):test,那么Angular給出的提示是:Unknown provider: testProvider <- test。
Constant比較特殊,我們稍后講解,我們先來看其他幾個。
Provider的聲明方式如下:
angular.module('com.ngnice.app').provider('greeting', function() { var _name = 'world'; this.setName = function(name) { _name = name; }; this.$get = function(/*這里可以放依賴注入變量*/) { return 'Hello, ' + _name; }; });
使用時:
angular.module('com.ngnice.app').controller('SomeCtrl', function($scope, greeting) { // 這里greeting應(yīng)該等于'Hello, world',怎么樣,你猜對了嗎? $scope.message = greeting; });
對Provider進(jìn)行配置時:
angular.module('com.ngnice.app').config(function(greetingProvider) { greetingProvider.setName('wolf'); });
容器的偽代碼如下:
var instance = diContainer['greeting']; // 先找是否已經(jīng)有了一個實例 if (!angular.isUndefined(instance)) { return instance; // 如果已經(jīng)有了一個實例,直接返回 } var ProviderClass = angular.module('com.ngnice.app').lookup('greetingProvider'); // 在服務(wù)名后面自動加上Provider后綴是Angular遵循的一項約定 var provider = new ProviderClass(); // 把Provider實例化 provider.setName('wolf'); instance = provider.$get(); // 調(diào)用$get,并傳入依賴注入?yún)?shù) diContainer['greeting'] = instance; // 把調(diào)用結(jié)果存下來 return instance;
事實上,如果不需要對name參數(shù)進(jìn)行配置,聲明代碼可以簡化為:
angular.module('com.ngnice.app').value('greeting', 'Hello, world');
這也就是需要這么多語法糖的原因。
下面給出其他語法糖的等價形式:
2.9.1 服務(wù)
angular.module('com.ngnice.app').service('greeting', function() { this.sayHello = function(name) { return 'Hello, ' + name; }; }); 等價于: angular.module('com.ngnice.app').provider('greeting', function() { this.$get = function() { var Greeting = function() { this.sayHello = function(name) { return 'Hello, ' + name; }; }; return new Greeting(); }; };
使用時:
angular.module('com.ngnice.app').controller('SomeCtrl', function($scope, greeting) { $scope.message = greeting.sayHello('world'); });
2.9.2 工廠
angular.module('com.ngnice.app').factory('greeting', function() { return 'Hello, world'; }); 等價于: angular.module('com.ngnice.app').provider('greeting', function() { this.$get = function() { var greeting = function() { return 'Hello, world'; }; return greeting(); } });
使用時:
angular.module('com.ngnice.app').controller('SomeCtrl', function($scope, greeting) { $scope.message = greeting; }); 在Angular源碼中,它們的實現(xiàn)是這樣的: function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } function service(name, constructor) { return factory(name, ['$injector', function($injector) { return $injector.instantiate(constructor); }]); } function value(name, val) { return factory(name, valueFn(val)); }
Angular提供了這么多種形式的服務(wù),那么我們在工程實踐中該如何選擇?我們可以遵循下列決策流程:
? 需要全局的可配置參數(shù)?用Provider。
? 是純數(shù)據(jù),沒有行為?用Value。
? 只new一次,不用參數(shù)?用Service。
? 拿到類,我自己new出實例?用Factory。
? 拿到函數(shù),我自己調(diào)用?用Factory。
但是,還有另一種更加敏捷的方式:
? 是純數(shù)據(jù)時,先用Value;當(dāng)發(fā)現(xiàn)需要添加行為時,改寫為Service;或當(dāng)發(fā)現(xiàn)需要通過計算給出結(jié)果時,改寫為Factory;當(dāng)發(fā)現(xiàn)需要進(jìn)行全局配置時,改寫為Provider。
? 最酷的是,這個過程對于使用者是透明的—它不需要因為實現(xiàn)代碼的改動而更改原有代碼。如上面Value和Factory的使用代碼,僅僅從使用代碼中我們區(qū)分不出它是Value還是Factory。
接下來,我們來看Constant。與其他Service不同,Constant不是Provider函數(shù)的語法糖。更重要的差別是,它的初始化時機(jī)非常早,可以在angular.module('com.ngnice.app'). config函數(shù)中使用,而其他的服務(wù)是不能被注入到config函數(shù)中的。這也意味著,如果你需要在config中使用一個全局配置項,那么它就只能聲明為常量,而不能聲明為變量。
在官方的開發(fā)指南中,給出了一個完整的對比表,見表2-1。
表2-1 服務(wù)指令對比表

下面給出解釋:
? 可以依賴其他服務(wù):由于Value和Constant的特殊聲明形式,顯然沒有進(jìn)行依賴注入的時機(jī)。
? 使用類型友好的注入:這條沒有官方的解釋,我的理解是—由于Factory可以根據(jù)程序邏輯返回不同的數(shù)據(jù)類型,所以我們無法推斷其結(jié)果是什么類型,也就是對類型不夠友好。Provider由于其靈活性比Factory更高,因此在類型友好性上和Factory是一樣的。
? 在config階段可用:只有Constant和Provider類型在config階段可用,其他都是Provider實例化之后的結(jié)果,所以只有config階段完成后才可用。
可用于創(chuàng)建函數(shù)/原生對象:由于Service是new出來的,所以其結(jié)果必然是類實例,也就無法直接返回一個可供調(diào)用的函數(shù)或數(shù)字等原生對象。
如果你確實需要對一個沒有提供Provider的第三方服務(wù)進(jìn)行配置,該怎么辦呢?Angular提供了另一種機(jī)制:decorator。這個decorator和前面提到過的裝飾器型指令沒有關(guān)系,它是用來改變服務(wù)的行為的。
比如我們有一個第三方服務(wù),名叫ui,它有一個prompt函數(shù),我們不能改它源碼,但需要讓它每次彈出提問框時都在控制臺輸出一條記錄,那么我們可以這樣寫:
angular.module('com.ngnice.app').config(function($provide) { // $delegate是ui的原始服務(wù) $provide.decorator('ui', function($delegate) { // 保存原始的prompt函數(shù) var originalPrompt = $delegate.prompt; // 用自己的prompt替換 $delegate.prompt = function() { // 先執(zhí)行原始的prompt函數(shù) originalPrompt.apply($delegate, arguments); // 再寫一條控制臺日志 console.log('prompt'); }; // 返回原始服務(wù)的實例,但也可以返回一個全新的實例 return $delegate; }) });
這種方式給你了超級靈活性,你可以改寫包括Angular系統(tǒng)服務(wù)在內(nèi)的任何服務(wù)—事實上,angular-mocks模塊就是使用decorator來MOCK $httpBackend、$timeout等服務(wù)的。
不過,如果你大幅修改了原始服務(wù)的邏輯,那么,這可能會給自己和維護(hù)者挖坑。俗話說,“不作死就不會死”。如果讓我來總結(jié)decorator的使用原則,那就是—慎用、慎用、慎用,如果確實想用,請務(wù)必遵循“Liskov代換”原則,并寫好單元測試。特別是,如果你想修改系統(tǒng)服務(wù)的工作邏輯,建議先多看幾遍文檔,確保你正確理解了它的每一個細(xì)節(jié)!
2.10 承諾
承諾(Promise)不是Angular首創(chuàng)的。作為一種編程模式,它出現(xiàn)在……1976年,比JavaScript還要古老得多。Promise全稱是 Futures and promises(未來與承諾)。要想深入了解,可以參見http://en.wikipedia.org/wiki/Futures_and_promises。
而在JavaScript世界中,一個廣泛流行的庫叫作Q(https://github.com/kriskowal/q)。而Angular中的$q就是從它引入的。
1.生活中的一個例子
Promise解決的是異步編程的問題,對于生活在同步編程世界中的程序員來說,它可能比較難于理解,這也構(gòu)成了Angular入門門檻之一,本節(jié)將用生活中的一個例子對此做一個形象的講解。
假設(shè)有一個家具廠,而它有一個VIP客戶張先生。
有一天張先生需要一個豪華衣柜,于是,他打電話給家具廠說:“我需要一個衣柜,回頭做好了給我送來”,這個操作就叫$q.defer(),也就是延期。因為這個衣柜不是現(xiàn)在要的,所以張先生這是在發(fā)起一個可延期的請求。
家具廠接下了這個訂單,給他留下了一個回執(zhí)號,并對他說:“我們做好了會給您送過去,放心吧”。這叫作Promise,也就是給了張先生一個“承諾”。
這樣,這個defer算是正式創(chuàng)建了,于是他把這件事記錄在自己的日記上,并且同時記錄了回執(zhí)號,這個變量叫作deferred,也就是已延期事件。
現(xiàn)在,張先生就不用再去想著這件事了,該做什么做什么,這就是“異步”請求的含義。
假設(shè)家具廠在一周后做完了這個衣柜,并如約送到了張先生家(包郵哦,親),這就叫作deferred.resolve(衣柜),也就是“問題已解決,這是您的衣柜”。而這時候張先生只要取出一下這個“衣柜”參數(shù)就行了。而且,這個“郵包”中也不一定只有衣柜,還可以包含別的東西,比如廠家宣傳資料、產(chǎn)品名錄等。整個過程中輕松愉快,誰也沒等誰,沒有浪費任何時間。
假設(shè)家具廠在評估后發(fā)現(xiàn)這個規(guī)格的衣柜我們做不了,那么它就需要deferred.reject(理由),也就是“我們不得不拒絕您的請求,因為……”。拒絕沒有時間限制,可以發(fā)生在給出承諾之后的任何時候,甚至可能發(fā)生在快做完的時候。而且拒絕時候的參數(shù)也不僅僅限于理由,還可以包含一個道歉信,違約金之類的。總之,你想給他什么就給他什么,如果你覺得不會惹惱客戶,那么不給也沒關(guān)系。
假設(shè)家具廠發(fā)現(xiàn),自己正好有一個符合張先生要求的存貨,它就可以用$q.when(現(xiàn)有衣柜)來兌現(xiàn)給張先生的承諾。于是,這件事立刻解決了,皆大歡喜。張先生可不在乎你是從頭做的還是現(xiàn)有的成品,只要達(dá)到自己的品質(zhì)要求就滿意了。
假設(shè)這個家具廠對客戶格外的細(xì)心,它還可以通過deferred.notify(進(jìn)展情況)給張先生發(fā)送進(jìn)展情況的“通知”。
這樣,整個異步流程圓滿完成!無論成功還是失敗,張先生都沒有往里面投入任何額外的時間成本。
好,我們再擴(kuò)展一下這個故事:
張先生又來訂貨了,這次他分多次訂了一張桌子,三把椅子,一張席夢思。但他不希望今天收到個桌子,明天收到個椅子,后天又得簽收一次席夢思,而是希望家具廠做好了之后一次性送過來,但是他當(dāng)初又是分別下單的,那么他就可以重新跟家具廠要一個包含上述三個承諾的新承諾,這就是$q.all([桌子承諾,椅子承諾,席夢思承諾]),這樣,他就不用再關(guān)注以前的三個承諾了,直接等待這個新的承諾完成,到時候只要一次性簽收了前面的這些承諾就行了。
2.回調(diào)地獄和Promise
通過上面這個生活中例子,相信作為讀者的你已經(jīng)了解到了異步和Promise的方式。為什么我們需要Promise呢?
JavaScript是一門很靈活的語言,由于它寄宿在瀏覽器中以事件機(jī)制為核心,所以在我們的JavaScript編碼中存在很多的回調(diào)函數(shù)。這是一個高性能的編程模式,所以它衍生出了基于異步I/O的高性能Nodejs平臺。但是如果不注意我們的編碼方法,那么我們就會陷入“回調(diào)地獄”,也有人稱為“回調(diào)金字塔”。嵌套式的回調(diào)地獄,代碼將會變得像意大利面條一樣。如下邊的嵌套回調(diào)函數(shù)一樣:
async1(function(){ async2(function(){ async3(function(){ async4(function(){ .... }); }); }); });
這樣嵌套的回調(diào)函數(shù),讓我們的代碼的可讀性變得很差,而且很難于調(diào)試和維護(hù)。所以為了降低異步編程的復(fù)雜性,開發(fā)人員一直尋找簡便的方法來處理異步操作。其中一種處理模式稱為Promise,它代表了一種可能會長時間運(yùn)行而且不一定必須完成的操作的結(jié)果。這種模式不會阻塞和等待長時間的操作完成,而是返回一個代表了承諾的(Promised)結(jié)果的對象。它通常會實現(xiàn)一種名叫then的方法,用來注冊狀態(tài)變化時對應(yīng)的回調(diào)函數(shù)。
Promise在任何時刻都處于以下三種狀態(tài)之一:未完成(pending)、已完成(resolved)和拒絕(rejected)三個狀態(tài)。以CommonJS Promise/A 標(biāo)準(zhǔn)為例,Promise對象上的then方法負(fù)責(zé)添加針對已完成和拒絕狀態(tài)下的處理函數(shù)。then方法會返回另一個Promise對象,以便于形成Promise管道,這種返回Promise對象的方式能夠讓開發(fā)人員把異步操作串聯(lián)起來,如then(resolvedHandler, rejectedHandler)。resolvedHandler回調(diào)函數(shù)在Promise對象進(jìn)入完成狀態(tài)時會觸發(fā),并傳遞結(jié)果;rejectedHandler函數(shù)會在拒絕狀態(tài)下調(diào)用。
所以我們上邊的嵌套回調(diào)函數(shù)可以修改為:
async1().then(async2).then(async3).catch(showError);
這下代碼看著清爽多了,我們不再需要忍受嵌套的無底深淵。
在ES6的標(biāo)準(zhǔn)版中已經(jīng)包含了Promise的標(biāo)準(zhǔn),很快它就將會從瀏覽器本身得到更好的支持。與此同時在ES6的標(biāo)準(zhǔn)版中,還引入了Python這類語言中的generator(迭代器的生成器)概念,它本意并不是為異步而生的,但是它擁有天然的yield暫停函數(shù)執(zhí)行的能力,并保存上下文,再次調(diào)用時恢復(fù)當(dāng)時的狀態(tài),所以它也被很好地運(yùn)用于JavaScript的異步編程模型中,其中最出名的案例當(dāng)屬Node Express的下一代框架KOA了。
最后還有個好消息,在ES7的標(biāo)準(zhǔn)中將有可能引入async和await這兩個關(guān)鍵詞,來更大的簡化我們的JavaScript異步編程模型。我們就可以如下的方式以同步的方式編寫我們的異步代碼:
async function sleep(timeout) { return new Promise((resolve, reject) => { setTimeout(function() { resolve(); }, timeout); }); } (async function() { console.log('做一些事情,' + new Date()); await sleep(3000); console.log('做另一些事情,' + new Date()); })();
3. Angular中的Promise
在Angular中大量使用著Promise,最簡單的是$timeout的實現(xiàn),我拷貝過來并加上了注釋:
function timeout(fn, delay, invokeApply) { // 創(chuàng)建一個延期請求 var deferred = $q.defer(), promise = deferred.promise, skipApply = (isDefined(invokeApply) && !invokeApply), timeoutId; timeoutId = $browser.defer(function() { try { // 成功,將觸發(fā)then的第一個回調(diào)函數(shù) deferred.resolve(fn()); } catch(e) { // 失敗,將觸發(fā)then的第二個回調(diào)函數(shù)或catch的回調(diào)函數(shù) deferred.reject(e); $exceptionHandler(e); } finally {
delete deferreds[promise.$$timeoutId]; } if (!skipApply) $rootScope.$apply(); }, delay); promise.$$timeoutId = timeoutId; deferreds[timeoutId] = deferred; // 返回承諾 return promise; } timeout.cancel = function(promise) { if (promise && promise.$$timeoutId in deferreds) { deferreds[promise.$$timeoutId].reject('canceled'); delete deferreds[promise.$$timeoutId]; return $browser.defer.cancel(promise.$$timeoutId); } return false; };
2.11 消息
在傳統(tǒng)的DOM編程中,消息(message)機(jī)制非常有用,特別是消息冒泡機(jī)制,讓我們不用額外的代碼就可以實現(xiàn)“職責(zé)鏈”模式。但是我們要盡量擺脫DOM操作,難道這是必須使用DOM操作的場景嗎?不是的,Angular中也有一種不依賴DOM的消息機(jī)制,本節(jié)中我們就對它進(jìn)行詳細(xì)講解。
我們知道,Scope也被組織成了一棵樹,跟DOM樹具有相似的結(jié)構(gòu)。Angular的消息機(jī)制就是通過scope上的幾個函數(shù)實現(xiàn)的:
? $broadcast(name, args):向當(dāng)前scope及其所有下級scope遞歸廣播名為name的消息,并帶上args參數(shù)。
? $emit(name, args):向當(dāng)前scope及其所有直線上級scope發(fā)送名為name的消息,并帶上args參數(shù)。
? $on(name, listener):監(jiān)聽本scope收到的消息,listener的形式為:function(event, args) {},event參數(shù)的結(jié)構(gòu)和DOM中的event類似。
以圖2-1所示的結(jié)構(gòu)的scope為例:
當(dāng)我們在rootScope上調(diào)用$broadcast廣播一個消息時,任何一個scope(包括rootScope)上通過$on注冊的listener都將收到這個消息。當(dāng)我們在scope1上調(diào)用$broadcast廣播一個消息時時,scope1/scope1.1/scope1.2將依次收到這個消息。當(dāng)我們在rootScope上調(diào)用$emit上傳一個消息時,rootScope將收到這個消息。當(dāng)我們在scope1.1上調(diào)用$emit上傳一個消息時,scope1.1/scope1/rootScope將依次收到這個消息。

圖2-1 scope樹
當(dāng)通過$emit上傳一個消息時,將使用冒泡機(jī)制,比如,假設(shè)我們在scope1.1上調(diào)用$emit,我們在scope1上注冊一個listener:
scope1.$on('name', function(event) { event.stopPropagation(); });
這個stopPropagation函數(shù)將阻止冒泡,也就是說scope1.1和scope1都將正常接收到這個消息,但rootScope就接收不到這個消息了。
有時候,用消息機(jī)制和普通回調(diào)函數(shù)都能達(dá)到類似的效果,如何選擇呢?
當(dāng)一個嵌套結(jié)構(gòu)具有樹形的業(yè)務(wù)含義時,我們就優(yōu)先使用消息機(jī)制來通訊。或者從另一個角度看,符合“職責(zé)鏈”模式的適用場景時,消息機(jī)制比較合適。反之,應(yīng)該使用普通的回調(diào)函數(shù)。
如果難以決定使用消息還是回調(diào)函數(shù),那么就優(yōu)先使用回調(diào)函數(shù)(主要是Angular事件),因為這種情況下執(zhí)行路徑比較明確,容易跟蹤。或者在對此有深入理解前,先使用表面的判斷方式:一個事件是否需要被很多地方處理?調(diào)用stopPropagation是否有意義?如果是,那么用消息,否則用回調(diào)。
2.12 單元測試
我們在第1章中已經(jīng)寫過兩個單元測試(unit test)了,這里我們簡單講一下理論知識。
在Angular中,單元測試的概念和傳統(tǒng)的后端編程是一樣的。也就是對某些小型功能塊兒進(jìn)行測試,保障其工作邏輯正常。單元測試要盡可能局部化,不要牽扯進(jìn)很多個模塊,必要時可進(jìn)行mock(模擬)。
2.12.1 MOCK的使用方式
由于JavaScript語言的動態(tài)特性,Mock一個普通對象不需要進(jìn)行特別處理。比如,如果一個測試函數(shù)需要訪問scope中的一個變量:name,但不用訪問$watch等scope的特有函數(shù),那么傳入一個普通的哈希對象{name: 'someName'}即可,并不需要new出一個scope來。
除了局部化以外,對單元測試來說,一大挑戰(zhàn)是網(wǎng)絡(luò)操作,如果使用真實的網(wǎng)絡(luò)操作,那么將帶來幾個問題:
? 網(wǎng)絡(luò)的不穩(wěn)定性,導(dǎo)致單元測試的不穩(wěn)定性。想象一個有時成功,有時失敗的單元測試,會讓程序員多么頭疼吧!
? 網(wǎng)絡(luò)響應(yīng)的速度會拖慢整體速度。單元測試執(zhí)行得必須盡可能快速,如果被迫由于網(wǎng)絡(luò)操作而變慢,那么一旦多了就會變得很慢,也就會有很多時間浪費在這里。
? 網(wǎng)絡(luò)的異步性。雖然異步調(diào)用對于單元測試來說并不是不可接受的,但是由于其返回時機(jī)不受控制,所以寫起來還是比同步調(diào)用復(fù)雜一些。
另一大挑戰(zhàn)是與時間有關(guān)的測試。比如一段代碼中設(shè)置了一小時后觸發(fā)的定時器,難道我們的單元測試就要一個小時后才能完成?這顯然是不合理的。
好在,Angular對網(wǎng)絡(luò)和定時器等進(jìn)行了封裝,變成了$http、$timeout、$interval等服務(wù)。這就意味著,我們只要使用這些內(nèi)置服務(wù)而不是setTimeout等原生函數(shù),那么我們就可以對它們進(jìn)行Mock,克服上述問題。
對于這些內(nèi)置服務(wù),Angular提供了一個獨立的測試庫:angular-mocks.js。
它對Angular的一些內(nèi)置服務(wù)進(jìn)行了Mock,比如$httpBackend、$timeout、$interval、$exceptionHandler、$log等服務(wù)。還提供了一些工具函數(shù),如用于加載模塊的module函數(shù)、用于依賴注入的inject函數(shù)、用于調(diào)試的dump函數(shù)等,這些函數(shù)都是頂層函數(shù),不需要加前綴就可以調(diào)用。
但Angular實際上沒有Mock $http服務(wù),而是Mock了XHR(XMLHttpRequest)對象,它把原來發(fā)送到服務(wù)端的Ajax調(diào)用,轉(zhuǎn)變成本地調(diào)用。這個通過本地調(diào)用來模擬服務(wù)器的對象則是$httpBackend(模擬http后端,也就是服務(wù)器)。
$httpBackend.whenGET('/someUrl').respond({name: 'wolf'}, {'X-Record-Count': 100});
上述語句聲明了一個模擬服務(wù)端,當(dāng)被測試代碼請求GET /someUrl這個地址時,將被$httpBackend攔截,并返回一個JSON對象{"name": "wolf"},同時,返回一個額外的response header:X-Record-Count,其值為"100"。
注意,我們這里其實只是定義了返回規(guī)則,并沒有規(guī)定啥時候返回這些數(shù)據(jù),也就是說,雖然被測代碼中的$http函數(shù)已經(jīng)能正確返回我們期望的數(shù)據(jù),但目前還不會被觸發(fā)—直到我們調(diào)用了$httpBackend.flush函數(shù)。這樣,我們就把測試中的異步調(diào)用變成了同步調(diào)用。
respond中的參數(shù)不但可以是一個或兩個哈希對象,還可以在前面增加一個返回碼,如respond(401, {message: 'Unauthorized'}, {'X-Sign-It': '1887a6b'})等,Angular會自動判斷它的數(shù)據(jù)類型,來決定使用哪種重載形式。如果你需要更多的控制力,還可以轉(zhuǎn)而傳入一個函數(shù),其原型是:function(method, url, data, headers) {},這個函數(shù)中的四個參數(shù)都是由$http請求發(fā)來的數(shù)據(jù),這個函數(shù)的返回值是一個數(shù)組,包含狀態(tài)碼、數(shù)據(jù)等信息,完整的范例如:
$httpBackend.whenPOST('/someUrl').respond(function(method, url, data, headers) { var result = 'Hello, ' + data.name; return [201, result, {'X-Greeting': 'Say Hello'}, 'OK']; });
這樣,當(dāng)被測代碼請求調(diào)用$http.post('/someUrl', {name: 'wolf'}),然后調(diào)用$httpBackend. flush()時,獲得的回應(yīng)為:狀態(tài)碼201,回應(yīng)體:Hello, wolf,回應(yīng)頭:X-Greeting: 'Say Hello',同時它的status text為OK。
不過,雖然這種形式很靈活,但對于單元測試來說,還是不應(yīng)該把mock邏輯寫得過于復(fù)雜,否則,如果測試代碼本身都可能出錯,會讓你的測試變得非常痛苦。寫mock時,推薦的最佳實踐是“給出固定數(shù)據(jù),返回固定數(shù)據(jù)”。
如果把上述代碼改寫為:$httpBackend.whenPOST('/someUrl', {name: 'wolf'}).respond ('Hello, wolf', {'X-Greeting': 'Say Hello'}, 'OK');,不但代碼量少了很多,而且更加簡潔明確,更能發(fā)揮“測試”作為“規(guī)約”的作用。
$timeout和$interval的Mock就比較簡單了,只是增加了一個flush函數(shù),它的作用和$httpBackend.flush一樣,也是立即觸發(fā)這個異步操作。
2.12.2 測試工具與斷言庫
我們寫好了測試,還要把它跑起來,用來跑測試的工具稱為Test Runner,Angular的范例工程中集成的測試工具是Karma,它的用法對寫測試來說幾乎可以不用管。
而代碼中用來寫斷言的庫稱為斷言庫,在范例工程中集成的是jasmine。我們測試代碼中的expect和toBe等函數(shù)都是來自它的。具體的使用方式可以參見它們的官方文檔,此處就不展開講解了。
2.13 端到端測試
端到端測試(e2e test),也稱為場景測試,它模擬的是用戶真實的操作場景:
? 用戶打開http://xxx地址。
? 在搜索框中輸入了abc。
? 然后點擊其后的搜索按鈕。
這時候,他期望看到一個列表,列出所有在標(biāo)題的任意位置包含了字符串a(chǎn)bc的條目,并且每條結(jié)果中的abc這三個字母被高亮。
所謂端到端,也就是一端是瀏覽器,另一端是服務(wù)器,這個測試貫通了前后端,具有近似于驗收測試的價值。
端到端測試不是什么新技術(shù),它在前后端分離架構(gòu)盛行之前就已經(jīng)被廣泛采用了,比如Selenium,而且Selenium也同樣可應(yīng)用于Angular中。
Angular的端到端測試工具稱為Protractor,事實上,它就是基于Selenium的,在Selenium的基礎(chǔ)上,它增加了一些Angular特有的元素選取方式,如根據(jù)ng-model選取元素等。
我的建議是,除非你所在的開發(fā)組織已經(jīng)把Angular作為唯一的前端選項,否則請使用Selenium中傳統(tǒng)的函數(shù),而不要使用Protractor特有的根據(jù)ng-model選取元素等函數(shù),這將讓你們的測試獨立于前端技術(shù)棧而被復(fù)用。
在我的工程實踐中,只會使用id、class等少數(shù)查閱方式,而不會根據(jù)ng-model等進(jìn)行查閱。并且,由于Angular的特點,被測試程序中可以不用任何id,所以,我們可以完全把id留給測試人員使用。如果寫測試的人員有權(quán)修改源碼,那么他/她可以自由的添加、刪除id,而不用擔(dān)心破壞了程序的邏輯和樣式。遵循這個約定,可以讓開發(fā)與測試的協(xié)同更加有效。
這里不展開講解,只把我在種子工程中寫的一些代碼加上注釋供大家自行研究:
1)pages/HomePage.js
// 這是一個頁面對象,用來封裝頁面中的元素和操作,以簡化規(guī)約代碼,并提供一定的變更隔離 module.exports = function() { this.title = function() { // browser對象封裝一組用來訪問瀏覽器屬性的函數(shù) return browser.getTitle(); }; // 根據(jù)id查找元素 this.name = element(by.id('name')); this.nameEcho = element(by.id('name-echo')); this.get = function() { // 控制瀏覽器訪問特定地址
browser.get('http://localhost:5000/#/'); }; };
2)demoSpec.js
// 取得頁面對象 var HomePage = require('./pages/HomePage'); describe('e2e范例,如果修改了首頁,請修改本測試 >', function () { var homePage; // 所有測試語句執(zhí)行之前,先在瀏覽器中打開它 beforeEach(function () { homePage = new HomePage(); homePage.get(); }); it('默認(rèn)的標(biāo)題是Showcase', function() { expect(homePage.title()).toBe('Showcase'); }); it('輸入用戶名后應(yīng)該回顯', function() { // 檢查初始狀態(tài)是否符合預(yù)期 expect(homePage.nameEcho.getText()).toBe('Hello,'); // 模擬用戶輸入 homePage.name.sendKeys('test'); // 檢查操作后狀態(tài)是否符合預(yù)期 expect(homePage.nameEcho.getText()).toBe('Hello,test'); }); });
- Advanced Splunk
- ASP.NET Web API:Build RESTful web applications and services on the .NET framework
- 國際大學(xué)生程序設(shè)計競賽中山大學(xué)內(nèi)部選拔真題解(二)
- 實用防銹油配方與制備200例
- 軟件測試項目實戰(zhàn)之性能測試篇
- Cassandra Data Modeling and Analysis
- iOS開發(fā)實戰(zhàn):從入門到上架App Store(第2版) (移動開發(fā)叢書)
- WordPress 4.0 Site Blueprints(Second Edition)
- Mastering Xamarin.Forms(Second Edition)
- Modern C++ Programming Cookbook
- Python預(yù)測分析與機(jī)器學(xué)習(xí)
- C語言程序設(shè)計教程
- 從零開始構(gòu)建深度前饋神經(jīng)網(wǎng)絡(luò):Python+TensorFlow 2.x
- Android初級應(yīng)用開發(fā)
- PhantomJS Cookbook