1. 程式人生 > >Think in AngularJS:對比jQuery和AngularJS的不同思維模式

Think in AngularJS:對比jQuery和AngularJS的不同思維模式

導言

stackoverflow上有一個人問了一個問題:如果我有jQuery背景,我應該如何切換到AngularJS的思維模式?

有一個回覆非常經典,獲得了兩千多票。

為了讓國內開發者也能領略到其中的核心思想,現把這個問題和答案翻譯出來供大家參考。

Question

假設我已經熟悉瞭如何使用jQuery來開發客戶端應用,我現在打算使用AngularJS。請描述一下有那些思維模式方面的東西需要轉變嗎?下面是舉出一些具體的問題,用來幫助你回答我的這個問題:

  1. 我應該以何種不同的方式來架構和設計客戶端web應用?最大的不同點是什麼?
  2. 我應該停止使用哪些東西;又應該開始使用哪些東西來替代?
  3. 服務端有沒有什麼需要考慮或者說需要約束的地方?

PS:我不想詳細對比jQuery和AngularJS之間的不同點。

Answer

1.不要預先設計頁面,然後再用DOM操作去修改它

在jQuery裡面,你會先設計好一個頁面,然後再讓它變成動態的。這是因為jQuery本身就是以混合使用的思路來設計的。基於這個簡單的前提,jQuery目前已經變得臃腫不堪。

但是在AngularJS的世界中,你心中首先必須有整體架構,然後從零開始構建應用。而不是一開始的時候就去想:“我已經有了這樣一塊DOM結構,我想讓它做×××”,你必須首先思考你到底要完成什麼功能,然後再開始動手,然後再設計你的應用,最後再去設計你的檢視。

2.不要混合使用jQuery和AngularJS

類似地,不要一開始就抱有這樣的想法:jQuery可以實現X、Y和Z,所以我只要在上面再覆蓋一層AngularJS,把模型和控制器加上即可。當你剛開始使用AngularJS的時候,這種想法實在誘人,這也是為什麼我總是建議AngularJS新手徹底拋棄jQuery的原因,直到他們習慣了以“Angular風格”去做事為止。

在這裡,以及在郵件列表裡面,我看到過很多這種精心設計的解決方案,其中包含150或者200行程式碼的jQuery外掛,然後他們再用一大堆回撥函 數和$apply把這些外掛粘到AngularJS上,這種做法非常複雜而且令人困惑不已;但是,他們最終居然能把這貨弄跑起來了!這裡的問題在於,在大 多數情況下,只需要很少的AngularJS程式碼就可以把這些jQuery外掛重寫一遍,然後所有事情都會突然變得簡潔明瞭起來。

底線是:在解決問題的過程中,首先“Think in AngularJS”(以AnguarJS的方式思考問題);如果你想不到解決方案,請求助於社群;如果在嘗試了所有這些方法之後還找不到簡單的解決方案,然後再求助於jQuery。但是,不要讓jQuery變成絆腳石,否則你永遠無法真正掌握AngularJS。

3.保持以架構的角度思考

首先要明確一點,單頁面應用是一種應用,它們不是web頁面。所以,我們需要像服務端開發者那樣去思考,而不是像客戶端開發者那樣思考。我們必須思考如何把我們的應用切分成獨立的、可擴充套件的、可測試的元件。

那麼,你怎麼才能做到這一點呢?你應該如何以AngularJS的方式思考問題呢?下面是一些基本的原則,與jQuery做個對比。

假設有一個叫做“官方記錄”(official record)的檢視

在jQuery裡面,我們會用程式設計的方式來修改檢視,我們會像下面這樣用ul標籤來定義一個下拉列表:

<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裡面,我們的應用邏輯中會像下面這行程式碼一樣來建立這個下拉列表:

$('.main-menu').dropdownMenu();

如果我們僅僅看檢視程式碼,我們無法立刻發現它有什麼功能。對於小型的應用來說,這樣做還算可以。但是對於大型應用來說,很快就會變得混亂並難以維護。

然而,在AngularJS中,檢視是一種功能,它是基於檢視的“官方記錄”。我們的ul宣告看起來就像下面這樣:

<ul class="main-menu" dropdown-menu>
    ...
</ul>

兩種實現方式的效果完全相同,但是在AngularJS的版本中,每一個看到模板的人都知道它在做什麼。 不管什麼時候開發團隊有新人進來,她看到這種程式碼之後就會立即明白,存在一個叫做dropdownMenu的指令,這個指令負責操控這個檢視。她憑直覺就可以知道答案,而沒有必要檢視任何程式碼。檢視本身已經告訴了我們這裡會發生什麼。這樣就更加清晰了。

AngularJS新手經常會問這樣的問題:我怎麼才能找出某種型別的所有超連結,然後在上面加上指令呢?我們會這樣回答他:你不應該這麼做。然後 他總是一副驚呆了樣子。你不應該這麼做的原因是:這是一種半jQuery半AngularJs式的思維方式,這不科學。用這種方式思考問題永遠得不到很好 的結果。你看到的應該是“官方記錄”。除了指令之外(不只包括以下程式碼),你永遠、永遠、永遠不應該去修改DOM。同時,指令會用在檢視上,這樣一來思路就清晰了。

記住:不要先設計,然後編寫標籤。你必須先架構,然後去設計。

資料繫結

到目前為止,這是AngularJS最讚的特性,利用這一特性可以省掉我在上一小節中提到的大量DOM操作程式碼。AngularJS會自動為你重新整理檢視,所以不需要你自己去做這件事!在jQuery中,我們會響應事件然後重新整理頁面內容。示例如下:

$.ajax({
  url: '/myEndpoint.json',
  success: function ( data, status ) {
    $('ul#log').append('<li>Data Received!</li>');
  }
});

對應的檢視程式碼如下:

<ul class="messages" id="log">
</ul>

這種方式除了會把注意點混在一起之外,還有我在前面所提到的思維模式問題。但是,更加重要的一點是,這樣做我們必須手動引用並更新DOM節點。同 時,如果我們想刪掉一個log物件,我們必須針對DOM重新進行編碼。這樣一來我們如何脫離DOM來測試這些邏輯呢?同時,如果我們要修改顯示效果又應該 怎麼做呢?

這樣有點兒亂,程式碼既瑣碎又脆弱。但是在AngularJS中,我們可以這樣做:

$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>

對於上面所提的刪除log物件這個問題,我們可以把檢視寫成這樣:

<div class="messages">
    <div class="alert" ng-repeat="entry in log">
        {{ entry.msg }}
    </div>
</div>

這裡我們用Bootstrap的alert塊替換了無序列表。並且我們永遠不需要修改控制器程式碼!同時更重要的是,無論何時或者何地更新了log物件,檢視都會自動**重新整理。高貴優雅!

雖然我沒有在這裡展示,其實資料繫結操作是雙向的。所以,在檢視中也可以編輯log資訊,只要這樣做即可:<input ng-model="entry.log"/>。另外還有更多驚喜。

區分資料模型層

在jQuery中,DOM有類似資料模型的意味。但是在AngularJS中,我們有一個獨立的資料模型層,我們可以按照自己的想法管理它,它和視 圖層完全獨立。對於前面例子中的資料繫結操作來說,這一點很有用,並且保持了注意點分離的原則,同時還可以引入更強的測試功能。在其它很多解答中都提到了 這一點,所以這裡我就不再贅述了。

注意點分離

以上所有一切都是為了實現這樣一個遠大的目標:讓你的注意點保持分離。你的檢視的角色是展示“官方記錄”所能進行的所有操作(絕大部分);你的資料 模型用來代表你的資料;你還有一個service層用來執行可複用的任務;你進行DOM操作並把指令混入到檢視中;最後你再用controller把所有 東西粘到一起。在其它很多回復裡面都提到了這一點,我唯一想要補充的一點是關於測試方面,在下面的小節中我會來討論它。

依賴注入

用來幫助我們實現注意點分離的特性就是依賴注入。如果你是從服務端語言轉過來的(例如從Java或者PHP),你可能對這個概念已經相當熟悉,但是如果你是一個前端仔,從jQuery轉過來的,你可能會覺得這種概念很愚蠢、很多餘、而且很裝逼。但事實並非如此。

從大的層面上講,DI意味著你可以自由地宣告元件,然後在其它元件中,你可以請求所宣告元件的例項,然後你就可以獲得它。你沒有必要知道載入順序、檔案路徑,以及諸如此類的東西。這種概念的強大能量可能不是那麼顯而易見,這裡我只舉一個(通用的)例子:測試。

比方說在我們的應用中,根據應用的狀態,我們需要通過一個REST API請求一個服務端的儲存實現,以及本地的儲存實現。當對我們的controller進行測試的時候,我們並不想和服務端進行通訊,畢竟我們正在測試的是controller而不是其它東西。我們可以僅僅新增一個虛擬的同名service作為前面所說的自定義元件,然後注射器將會保證controller能夠自動獲得這個虛擬的服務,我們的controller不會知道它們之間有什麼不同,也沒有必要知道。

關於測試再多說一點...

4.測試驅動開發---永遠

這裡的內容是關於架構方面的,實際上應該屬於第三小節,但是這塊內容極其重要,所以我把它獨立成了一個單獨的小節。

在你所見過、用過,或者寫過的所有jQuery外掛中,它們有多少個帶有完整的測試用例?不是很多,因為jQuery不是太鳥這個原則。但是AngularJS非常看重這一點。

在jQuery中,唯一能夠進行測試的方式通常是在一個sample/demo頁面上建立獨立的元件,通過這個頁面我們可以進行DOM操作相關的測 試。所以,這樣一來我們必須獨立開發一個元件,然後再把它整合到我們的應用中去。好麻煩!在使用jQuery進行開發的時候,消耗的時間太多了,這是因為 我們選擇了迭代的方式,而不是選擇測試驅動開發的方式。如此一來,誰又能責怪我們呢?

但是,在AngularJS中,由於我們分離了注意點,所以我們可以用迭代的方式進行測試驅動開發!例如,比方說我們需要一個超級簡單的指令,用來在選單中顯示當前的路由是什麼。我們可以這樣在檢視中宣告所需要的東西:

<a href="/hello" when-active>Hello</a>

好,現在我們來編寫一個單元測試:

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();
}));

我們執行這個單元測試,確認它是否會失敗。然後我們再來編寫指令:

.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' );
                }
            });
        }
    };
});

現在,我們的測試執行通過了,並且選單的行為符合了我們的預期。這樣一來,我們的開發既是可迭代的,也是測試驅動的。碉堡了。

5.從概念上說,指令並非打包好的jQuery

你經常會聽到“只能在指令中操作DOM”之類的言論。這是必須的。請慎重對待這一原則。

我們再來稍微深入一點...

有一些指令只是用來裝飾一下視圖裡面已經存在的內容(想想ngClass),有時候也會直接進行一些DOM操作,然後就沒有然後了。但是,像 “widget”(小元件)這樣帶有模板的指令,它同樣需要遵守注意點分離的原則。也就是說,模板自身同樣需要保持很強的獨立性,獨立於link和 controller函式的具體實現。

AngularJS內建了完整的工具,讓實現這一點非常容易;我們可以使用ngClass指令來動態更新CSS樣式類;ngBind可以用來做雙向 資料繫結;ngShow和ngHide可以以程式設計的方式來顯示或者隱藏元素;諸如此類還有很多。我們也可以匯入我們自己所編寫的指令。換句話說,我們可以 實現各種絢麗的效果而不需要進行DOM操作。進行的DOM操作越少,指令測試起來就越容易、設定樣式就越容易、在未來修改起來也會越容易、並且可複用性和 可分發性也會更好。

我看到很多AngularJS新手把指令當成容納各種jQuery程式碼的場所。換句話說,他們的想法是:“既然我不能在控制器裡面做DOM操作,那我就把DOM操作相關的程式碼放到指令裡面好了”。這種做法確實是好一些了,但是通常還是是錯誤的

思考一下我們在第三小節裡面所編寫的logger應用。即使我們把相關的操作放到了指令裡面,我們還是用一種“AngularJS的方式”來實現了 它。它仍然沒有做任何DOM操作!在很多情況下DOM操作是必須的,但是這種情況比你想象的要少得多!當你在應用裡面的任何地方進行DOM操作之前,請問 問自己,是不是真的必須要這樣做。很有可能存在更好的實現方式。

下面是一個小例子,用來說明我經常看到的一種模式。我們需要一個開關型的按鈕。(注意:這個例子的程式碼有點裝逼,並且有點冗長,只是為了用來代表更加複雜一些的例子,這些例子通常是以與此相同的方式來解決的。)

.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;
            });
        }
    };
});

這段程式碼裡面有很多錯誤的地方。

第一,jQuery從來就不是必須的。我們這裡要實現的東西實際上完全不需要jQuery!

第二,即使我們已經在頁面上引入了jQuery,也沒有必要在這裡去使用;對於沒有使用jQuery的專案,我們可以簡單地使用angular.element,這樣一來我們的元件同樣能夠很好地執行。

第三,假設這裡必須使用jQuery我們的指令才能執行,jqLite(angular.element)總是會自動使用jQuery,如果jQuery已經載入了話!所以我們不需要使用$,我們只要使用angular.element就可以了。

第四,與第三點類似,jqLite元素沒有必要使用$來進行包裝,傳遞給link函式的element已經是一個jQuery元素了!

還有第五點,這一點我們在前面的小節中沒有提到,那就是我們為什麼要把模板相關的內容混合在我們的程式碼邏輯裡面?

以上指令可以重寫成下面這樣(即使對於非常複雜的情況同樣可以改寫!),改寫之後程式碼極其簡單:

.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;
            };
        }
    };
});

再說一次,模板相關的內容位於template中,所以你(或者你的使用者)可以簡單地切換它,從而可以滿足任何必要的樣式要求,同時永遠不需要去修改程式碼邏輯。可複用性---嘭!

這樣改寫之後還會帶來其它好處,比如測試---這是必須的!不管模板裡面是什麼內容,指令內部的API永遠不需要修改,這樣一來重構就非常簡單了。你可以隨意修改模板的內容而沒有必要去理睬指令。同時無論你修改了什麼內容,你的測試依然能夠執行通過。

w00t!

好吧,如果指令並非jQuery函式之類的集合,那麼它們是什麼呢?實際上指令是HTML擴充套件。如果HTML無法做到你想實現的某件事情,你就自己編寫一個指令,然後再去使用這個指令,好像它就是HTML的一部分一樣。

換句話說,如果AngularJS沒有內建支援某件事情,請思考一下你的團隊應該怎麼樣去實現它,參照ngClick,ngClass等指令的做法。

小結

不要使用jQuery。最好不要引入它。它只會拖你的後腿。當你遇到一個問題,而這個問題你知道如何使用jQuery去解決,那麼在你使用$之前, 請思考一下如何以AngularJS的方式去解決它。如果你不知道,去問別人!最好的解決方式十有八九不需要使用jQuery,如果你用jQuery的方 式來解決,最終會給你帶來更多工作量。

其它相關內容:

1、OReilly的《AngularJS》已由電子工業出版社出版

http://damoqiongqiu.iteye.com/blog/1965167

2、《AngularJS》5個例項詳解Directive(指令)機制

3、AngularJS表單基礎

4、AngularJS Form 進階:遠端校驗和自定義輸入項

5、AngularJS:在Windows上安裝Yeoman

6、對比Angular/jQueryUI/Extjs:沒有一個框架是萬能的

7、使用JsTestDriver實現JavaScript單元測試

8、JavaScript單元測試系列二:將Jasmine整合到JsTestDriver


原文地址:http://damoqiongqiu.iteye.com/blog/1926475