有jQuery背景的我,該如何用AngularJS編程思想?
導(dǎo)讀:本文由機(jī)器human 編譯自 Mark Rajcok 在 StackOverflow 的同名問(wèn)答題《How do I “think in Angular.js” if I have a jQuery background?》。下面是 Mark Rajcok 的提問(wèn):
“我可以熟練使用jQuery進(jìn)行客戶端應(yīng)用的開(kāi)發(fā),但是現(xiàn)在我希望開(kāi)始使用Angular.js。哪位能描述一下這個(gè)過(guò)程中必要的模式變化嗎?希望您的答案能夠圍繞下面這些具體的問(wèn)題:
1. 我如何對(duì)客戶端web應(yīng)用進(jìn)行不同方式的架構(gòu)和設(shè)計(jì)?它們之間最大的區(qū)別是什么?(譯者注:指jQuery和Angular.js)
2. 有什么是我不該做或者不該使用的;而又有什么是我應(yīng)該做或者應(yīng)該使用的呢?
3. 有沒(méi)有一些服務(wù)端的考量/約束呢?
我在尋找的就是一個(gè)關(guān)于jQuery和Angular.js之間的詳細(xì)的比較。”
下面是來(lái)自 Josh David Miller 的最佳回答:
1. 絕不要先設(shè)計(jì)你的頁(yè)面,然后用DOM操作去改變它
在jQuery中,你會(huì)先設(shè)計(jì)一個(gè)頁(yè)面,然后讓它變得動(dòng)態(tài)化。這是因?yàn)閖Query是為了擴(kuò)展而設(shè)計(jì)的,并在這個(gè)前提下變得越來(lái)越臃腫。
但是在Angular.js中, 你必須從一開(kāi)始就在腦子里掛著架構(gòu)的弦。不要一開(kāi)始就想著“我有這樣一個(gè)DOM,我想讓它做X”, 你必須從你要完成的目標(biāo)開(kāi)始思考,然后設(shè)計(jì)你的應(yīng)用, 最后才是設(shè)計(jì)你的視圖。
2. 不要用 Angular.js 擴(kuò)展 jQuery
類(lèi)似地,不要一開(kāi)始就帶著這樣的想法:jQuery可以完成X,Y,Z,所以我只要在其上為模型和控制器添加Angular.js就行了。在起步階段這確實(shí)很容易勾引你,這也是為什么我總是推薦Angular.js新手根本不要使用jQuery,至少要在他們習(xí)慣了“angular 方式”之后。
我在這里(譯者注:指stackoverflow)和郵件列表上看到過(guò)很多開(kāi)發(fā)者,他們用150或者200行代碼的jQuery插件,然后利用一堆讓人困惑的復(fù)雜的回調(diào)和$apply與Angular.js粘合起來(lái)建立這些詳盡的解決方案;最終確實(shí)可以跑起來(lái)! 但是其實(shí)這個(gè)問(wèn)題在大多數(shù)情況下,我們可以用一小段Angular.js代碼來(lái)重寫(xiě)jQuery插件即可,而這種方式會(huì)讓一切剎那間簡(jiǎn)單明了可理解。
我覺(jué)得這類(lèi)問(wèn)題的底線是:當(dāng)你在解決問(wèn)題時(shí),首先利用“Angular.js思想”去做;如果你不能想出一個(gè)方案,那么就在社區(qū)里詢問(wèn);如果還是沒(méi)有簡(jiǎn)單的解決方法,那么再請(qǐng)隨意使用jQuery吧。但是注意,千萬(wàn)別讓jqeury成為你的拐杖,不然你將永遠(yuǎn)無(wú)法真正精通Angular.js。
3. 永遠(yuǎn)根據(jù)架構(gòu)去思考
首先你要知道,單頁(yè)面結(jié)構(gòu)也是應(yīng)用。它不是網(wǎng)頁(yè)。所以我們需要有服務(wù)端開(kāi)發(fā)者思想加上客戶端開(kāi)發(fā)者思想。 我們必須考慮如何將我們的應(yīng)用拆分為獨(dú)立,可擴(kuò)展,可測(cè)試的組件。
那么你要怎么做呢?你如何做到利用“angualrjs思想”呢?這里有一些普遍的原則,與jQuery作為比照。
視圖是“正式記錄”
在jQuery中,我們通過(guò)編程方式來(lái)改變視圖。我們可以像下面這樣通過(guò)ul標(biāo)簽來(lái)定義一個(gè)下拉菜單:
- <ul class="main-menu">
- <li class="active">
- <a href="#/home">Home</a>
- </li>
- <li>
- <a href="#/menu1">Menu 1</a>
- <ul>
- <li><a href="#/sm1">Submenu 1</a></li>
- <li><a href="#/sm2">Submenu 2</a></li>
- <li><a href="#/sm3">Submenu 3</a></li>
- </ul>
- </li>
- <li>
- <a href="#/home">Menu 2</a>
- </li>
- </ul>
在jQuery中,根據(jù)我們應(yīng)用的邏輯,可以用類(lèi)似下面的語(yǔ)句來(lái)激活它。
- $('.main-menu').dropdownMenu();
當(dāng)我們只是看著視圖的時(shí)候,不會(huì)立刻看出它的功能。對(duì)于小應(yīng)用而言,這樣是沒(méi)問(wèn)題的。但是對(duì)于大型的應(yīng)用,情況就一下子變得令人困惑并且難以維護(hù)。
但是在Angular.js中,視圖是基于視圖的功能的正式記錄。我們的ul是像下面這樣聲明的:
- <ul class="main-menu" dropdown-menu>
- ...
- </ul>
這兩者其實(shí)做了同樣的事情,但是在Angular.js的版本中,任何看到這個(gè)模板的人都知道將要發(fā)生什么。不論何時(shí),開(kāi)發(fā)團(tuán)隊(duì)里有任何新的開(kāi)發(fā)人員加入,她可以一眼看出有一個(gè)叫做dropdownMenu的指令作用在視圖上;她根本不需要憑直覺(jué)猜測(cè)或者研究下代碼才找到正確的答案。視圖本身就告訴我們將會(huì)發(fā)生什么了。清晰多了。
angualrjs的新手經(jīng)常會(huì)問(wèn)這樣一個(gè)問(wèn)題: 我如何找到某一類(lèi)所有的鏈接并且給它們添加一個(gè)指令呢?當(dāng)看到我們回復(fù)的時(shí)候小伙伴都震驚了:壓根別去這樣做。但是勸你不要這樣做的原因是,這樣做就像是一半jQuery,一半angulrjs,而這真心很糟。這里的問(wèn)題是,開(kāi)發(fā)者想在angualrjs的情境中使用jQuery方式。而這絕對(duì)不會(huì)玩得轉(zhuǎn)。視圖是正式記錄。超出指令的范圍(這點(diǎn)下文會(huì)談?wù)摳啵?,你絕不要去改變DOM。而且指令是應(yīng)用在視圖中的,目的自然也一目了然。
記?。翰灰仍O(shè)計(jì)再修飾。你必須先進(jìn)行架構(gòu),然后再考慮設(shè)計(jì)。
數(shù)據(jù)綁定
這是Angular.js目前最酷的特性之一,并且秒殺我前文提到的各種需要的DOM操作。不需要你自己動(dòng)手,Angular.js將自動(dòng)更新你的視圖有木有!
在jQuery里, 我們響應(yīng)事件并更新內(nèi)容,大概是這個(gè)樣子:
- $.ajax({
- url: '/myEndpoint.json',
- success: function ( data, status ) {
- $('ul#log').append('<li>Data Received!</li>');
- }
- });
視圖則看上去是這樣的:
- <ul class="messages" id="log">
- </ul>
除了關(guān)注點(diǎn)混合的問(wèn)題,這里同樣有之前提到的表征目的的問(wèn)題。更重要的是,我們不得不手動(dòng)引用并更新dom節(jié)點(diǎn)。并且如果我們想要?jiǎng)h除一個(gè)日志,我們不得不再次對(duì)dom編程操作。我們?cè)鯓硬拍軖侀_(kāi)dom來(lái)測(cè)試邏輯呢?還有,如果我們希望改變展現(xiàn)呢?
#p#
真是讓人凌亂。。。
但是在Angular.js中,我們可以這樣做:
- $http( '/myEndpoint.json' ).then( function ( response ) {
- $scope.log.push( { msg: 'Data Received!' } );
- });
我們的視圖看上去是這樣的:
- <ul class="messages">
- <li ng-repeat="entry in log">{{ entry.msg }}</li>
- </ul>
但是考慮到剛才提到的問(wèn)題,我們的視圖看上去可以是這樣的:
- <div class="messages">
- <div class="alert" ng-repeat="entry in log">
- {{ entry.msg }}
- </div>
- </div>
現(xiàn)在,替換掉了無(wú)序列表,我們使用Bootstrap警告框。同時(shí)我們根本不需要改變控制器代碼!更重要的是,不論日志何時(shí)或者如何更新,視圖也會(huì)跟著改變。自動(dòng)的!漂亮!
雖然我沒(méi)有在這里演示出來(lái),但是數(shù)據(jù)綁定是雙向的。所以這些日志信息同樣可以在視圖中被編輯,就像這樣:
- <input ng-model="entry.msg" />
是不是更開(kāi)心了?
不同的模型層
在jQuery中,dom有點(diǎn)像模型。但是在angualrjs中,我們有一個(gè)分離的模型層, 而這個(gè)模型層可以讓我們用任何方式管理,完全獨(dú)立于視圖。這對(duì)于上面說(shuō)的數(shù)據(jù)綁定很有幫助, 還可以維護(hù)關(guān)注點(diǎn)分離,并且引入更多的可測(cè)試性。其它的答案提到了這點(diǎn),所以我這里就不再贅述了。
關(guān)注點(diǎn)分離
以上所有的這些把我們帶入了這樣的主題:保持你的關(guān)注點(diǎn)分離。你的視圖表現(xiàn)的像記錄什么會(huì)發(fā)生(大部分情況)的正式記錄;你的模型表現(xiàn)你的數(shù)據(jù);你有一個(gè)服務(wù)層來(lái)執(zhí)行可重用的任務(wù);你執(zhí)行dom操作并通過(guò)指令擴(kuò)展你的視圖;并且你用控制器來(lái)組合這些。這些同樣已經(jīng)在其它答案中提到,我在這里唯一還要提出的一個(gè)事情就是可測(cè)試性,我會(huì)在下文的另一節(jié)里專(zhuān)門(mén)討論。
依賴注入
依賴注入是讓我們實(shí)現(xiàn)關(guān)注點(diǎn)分離的方法。如果你是一個(gè)服務(wù)器端的開(kāi)發(fā)者(從java到php),你可能對(duì)這個(gè)概念已經(jīng)非常熟悉了,但是如果你是一個(gè)來(lái)自jQuery的客戶端的朋友,那么你可能會(huì)認(rèn)為這個(gè)概念是傻淺挫。但是它可不是:)
從一個(gè)更廣的觀點(diǎn)來(lái)看, 依賴注入意味著你可以非常自由的聲明組件,然后你可以通過(guò)任意其它組件,呼叫一個(gè)它的實(shí)例,然后授權(quán)。
你不需要知道載入順序,或者文件位置,或者其它類(lèi)似的東西。這種強(qiáng)大的力量可能不會(huì)立刻顯現(xiàn),但是我這里會(huì)提供一個(gè)(通常)的例子:測(cè)試。
比如在我們的應(yīng)用中,需要一個(gè)通過(guò)REST API,同時(shí)也依賴于應(yīng)用狀態(tài),本地存儲(chǔ)實(shí)現(xiàn)了服務(wù)器端存儲(chǔ)的服務(wù)。當(dāng)在我們的控制器上跑測(cè)試的時(shí)候,我們不希望與服務(wù)器端通訊-畢竟我們?cè)跍y(cè)試控制器。我們能夠僅僅添加一個(gè)與我們?cè)冀M件同名的mock服務(wù),注入器將確保我們的控制器自動(dòng)獲取偽造對(duì)象–我們的控制器不會(huì)也不需要知道它們的區(qū)別。
那么既然提到測(cè)試……
4. 保持測(cè)試驅(qū)動(dòng)的開(kāi)發(fā)
這個(gè)其實(shí)是關(guān)于架構(gòu)的第三節(jié)的一部分,但是這個(gè)主題非常重要,所以我需要將其提出來(lái)作為自成體系的部分。
那些你看過(guò),用過(guò),寫(xiě)過(guò)的所有jQuery插件,它們中有多少有相應(yīng)的測(cè)試包?不是很多吧? 因?yàn)閖Query可不是很遵守這個(gè)規(guī)矩。但是angualrjs是。
在jQuery中, 測(cè)試的唯一方法是用示例頁(yè)面來(lái)獨(dú)立地創(chuàng)建組件,針對(duì)該頁(yè)面我們的測(cè)試可以實(shí)施dom操作。于是我們就不得不分離地開(kāi)發(fā)一個(gè)組件然后將其集成進(jìn)我們的應(yīng)用。多么不方便?。?/p>
那么多時(shí)間啊,當(dāng)我們使用jQuery開(kāi)發(fā)時(shí),我們選擇使用迭代開(kāi)發(fā)代替測(cè)試驅(qū)動(dòng)的開(kāi)發(fā)??梢哉l(shuí)又能怪我們呢?
但是因?yàn)槲覀冇嘘P(guān)注點(diǎn)分離,我們能夠在Angular.js里反復(fù)使用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)。舉個(gè)例子,我們想要一個(gè)超簡(jiǎn)單的指令來(lái)指示在菜單中我們目前的路徑是什么。我們可以在視圖中這樣聲明我們想獲取的:
- <a href="/hello" when-active>Hello</a>
好了,我們現(xiàn)在可以寫(xiě)個(gè)測(cè)試:
- it( 'should add "active" when the route changes', inject(function() {
- var elm = $compile( '<a href="/hello" when-active>Hello</a>' )( $scope );
- $location.path('/not-matching');
- expect( elm.hasClass('active') ).toBeFalsey();
- $location.path( '/hello' );
- expect( elm.hasClass('active') ).toBeTruthy();
- }));
我們運(yùn)行測(cè)試,并確認(rèn)它是失敗的。那么我們來(lái)寫(xiě)下我們的指令:
- .directive( 'whenActive', function ( $location ) {
- return {
- scope: true,
- link: function ( scope, element, attrs ) {
- scope.$on( '$routeChangeSuccess', function () {
- if ( $location.path() == element.attr( 'href' ) ) {
- element.addClass( 'active' );
- }
- else {
- element.removeClass( 'active' );
- }
- });
- }
- };
- });
現(xiàn)在我們的測(cè)試通過(guò)了,并且我們的菜單按照請(qǐng)求運(yùn)行。我們的開(kāi)發(fā)是迭代并且測(cè)試驅(qū)動(dòng)的,太酷了哦!
#p#
5. 從概念上來(lái)看,指令不是打包的jQuery
你會(huì)經(jīng)常聽(tīng)到“只在指令里做dom操作”。這是必要的。請(qǐng)對(duì)它表示出尊重!
但是讓我們談點(diǎn)更深入的。。。
一些指令只是裝飾那些在視圖里的已有的(想想ngClass),因此有時(shí)候就直接進(jìn)行dom操作,基本上都能搞定。但是如果一個(gè)指令像個(gè)“widget”并有一個(gè)模板,
那它同樣要遵守關(guān)注點(diǎn)分離原則。也就是說(shuō),這個(gè)模板也應(yīng)該與它在鏈接和控制器函數(shù)里的實(shí)現(xiàn)保持最大的獨(dú)立。
Angular.js自帶著一套工具讓這件事變得簡(jiǎn)單; 使用ngClass我們能夠動(dòng)態(tài)的更新類(lèi);ngBind允許雙向的數(shù)據(jù)綁定;ngShow和ngHide以編程的方式顯示或隱藏一個(gè)元素;還有更多–包括我們自己寫(xiě)的那些。換句話說(shuō), 我們可以不用DOM操作來(lái)實(shí)現(xiàn)所有的酷炫的事兒。 dom操作越少,指令越容易測(cè)試,它們也更容易樣式化,在未來(lái)它們也更容易改變,并且也變得更加可重用和可分發(fā)。
我看到很多Angular.js開(kāi)發(fā)新手將指令當(dāng)做放置一堆jQuery的地方。換句話說(shuō), 他們認(rèn)為:“既然我不能在控制器里做dom操縱,那么我就把這段代碼放到指令里”。當(dāng)然這看上去好多了,但是通常這仍然是錯(cuò)的。
想一下在第三節(jié)里我們編寫(xiě)的日志記錄。即使我們將其放到一個(gè)指令里,我們?nèi)匀幌M?ldquo;anggualjs方式”來(lái)做這件事情。這仍然沒(méi)有做任何 dom操作!在很多情況下dom操作是必須的,但是這種情況其實(shí)比你想的少得多!在你在你的應(yīng)用中到處使用dmo操作之前,問(wèn)問(wèn)你自己,這真的是必須的嗎??赡苡懈玫姆椒亍?/p>
這里用一個(gè)簡(jiǎn)單的例子來(lái)展示我們經(jīng)常會(huì)看到的一個(gè)模式。我們需要一個(gè)切換按鈕。(注意:這個(gè)例子有那么一點(diǎn)人為設(shè)計(jì)的技巧并且有點(diǎn)啰嗦,但是很多更復(fù)雜的情況其實(shí)也完全可以根據(jù)這個(gè)例子的方式來(lái)解決。)
- .directive( 'myDirective', function () {
- return {
- template: '<a class="btn">Toggle me!</a>',
- link: function ( scope, element, attrs ) {
- var on = false;
- $(element).click( function () {
- if ( on ) {
- $(element).removeClass( 'active' );
- }
- else {
- $(element).addClass( 'active' );
- }
- on = !on;
- });
- }
- };
- });
這里有一些錯(cuò)誤。第一,jQuery不是必須的。我們這里做的一切都不需要jQuery!第二, 即使是我們的頁(yè)面上已經(jīng)有jQuery,也沒(méi)有理由一定要在這里使用它;我們可以簡(jiǎn)單的使用angular.element,而且就算在一個(gè)沒(méi)有 jQuery的項(xiàng)目中我們的組件仍然可以工作。第三,即使我們假設(shè)為了讓這個(gè)指令工作,jQuery是必須的,如果jqury被加載,那么 jqLite(angler.element)一定會(huì)使用jQuery。所以我們不需要使用$(譯者注:jQuery的一個(gè)標(biāo)識(shí)符號(hào),是jQuery函數(shù)的別名)–我們可以使用angular.element。第四, 緊接第三點(diǎn),jqLite元素不需要被包裹在$里–傳遞給link函數(shù)的element已經(jīng)是一個(gè)jQuery元素!第五, 我上一節(jié)已經(jīng)提過(guò)的,為什么我們要將模板混合進(jìn)我們的邏輯呢?
這個(gè)指令可以更精簡(jiǎn)的重寫(xiě)(即時(shí)在更復(fù)雜的例子里也一樣):
- .directive( 'myDirective', function () {
- return {
- scope: true,
- template: '<a class="btn" ng-class="{active: on}" ng-click="toggle()">Toggle me!</a>',
- link: function ( scope, element, attrs ) {
- scope.on = false;
- scope.toggle = function () {
- scope.on = !$scope.on;
- };
- }
- };
- });
再次強(qiáng)調(diào),模板的那些代碼都是在模板里的,所以你或者你的使用者能夠很方便的將其換成一個(gè)符合任何需要的樣式,同時(shí)邏輯不被改變。這就是重用性-贊!
當(dāng)然,還有其它很多好處–比如測(cè)試 – 這很容易! 不論什么在模板中,指令的內(nèi)部API絕不會(huì)被接觸,所以重構(gòu)就變得容易。你可以在不接觸指令的情況下隨意改變你的模板。并且不論你怎么變,你的測(cè)試仍可以通過(guò)。
耶!
所以如果指令不只是一組jQuery風(fēng)格的函數(shù),那么它們是什么呢?指令其實(shí)就是html的擴(kuò)展。如果html不能完成你希望它做的一些事情,你就寫(xiě)一個(gè)指令來(lái)做,并且當(dāng)它是html的一部分來(lái)使用。
換句話說(shuō), 如果Angular.js一下子無(wú)法完成手頭的工作,想想看你的團(tuán)隊(duì)是否能用ngClick、ngClass等來(lái)搞定它。
總結(jié)
別用jQuery。甚至都別引用它。它會(huì)阻礙你。當(dāng)你有個(gè)問(wèn)題已經(jīng)知道怎么用jQuery來(lái)解決的時(shí)候,在你將手伸向$的時(shí)候,試試能不能在Angular.js的規(guī)范內(nèi)解決。如果你不知道,就問(wèn)!
十有八九最佳的方法是不需要jQuery的,而當(dāng)你選擇用jQuery的時(shí)候反而會(huì)導(dǎo)致更多的工作呢。
原文鏈接: stackoverflow 翻譯: 伯樂(lè)在線 - jiqihuman




























