AngularJS 開發中常犯的10個錯誤

edithfang發表於2014-11-05
以下這份列表摘取了常見的一些AngularJS的錯誤用法,尤其是在app開發過程中。

1. MVC目錄結構

AngularJS,直白地說,就是一個MVC框架。它的模型並沒有像backbone.js框架那樣定義的如此明確,但它的體系結構卻恰如其分。當你工作於一個MVC框架時,普遍的做法是根據檔案型別對其進行歸類:
templates/
    _login.html
    _feed.html
app/
    app.js
    controllers/
        LoginController.js
        FeedController.js
    directives/
        FeedEntryDirective.js
    services/
        LoginService.js
        FeedService.js
    filters/
        CapatalizeFilter.js


看起來,這似乎是一個顯而易見的結構,更何況Rails也是這麼幹的。然而一旦app規模開始擴張,這種結構會導致你一次需要開啟很多目錄,無論你是使用sublime,Visual Studio或是Vim結合Nerd Tree,你都會投入很多時間在目錄樹中不斷地滑上滑下。

與按照型別劃分檔案不同,取而代之的,我們可以按照特性劃分檔案:
app/
    app.js
    Feed/
        _feed.html
        FeedController.js
        FeedEntryDirective.js
        FeedService.js
    Login/
        _login.html
        LoginController.js
        LoginService.js
    Shared/
        CapatalizeFilter.js


這種目錄結構使得我們能夠更容易地找到與某個特性相關的所有檔案,繼而加快我們的開發進度。儘管將.html和.js檔案置於一處可能存在爭議,但節省下來的時間更有價值。

2. 模組

將所有東西都一股腦放在主模組下是很常見的,對於小型app,剛開始並沒有什麼問題,然而很快你就會發現坑爹的事來了。
var app = angular.module('app',[]);
app.service('MyService', function(){
    //service code
});
app.controller('MyCtrl', function($scope, MyService){
    //controller code
});
在此之後,一個常見的策略是對相同型別的物件歸類。
var services = angular.module('services',[]);
services.service('MyService', function(){
    //service code
});

var controllers = angular.module('controllers',['services']);
controllers.controller('MyCtrl', function($scope, MyService){
    //controller code
});

var app = angular.module('app',['controllers', 'services']);
這種方式和前面第一部分所談到的目錄結構差不多:不夠好。根據相同的理念,可以按照特性歸類,這會帶來可擴充套件性。
var sharedServicesModule = angular.module('sharedServices',[]);
sharedServices.service('NetworkService', function($http){});

var loginModule = angular.module('login',['sharedServices']);
loginModule.service('loginService', function(NetworkService){});
loginModule.controller('loginCtrl', function($scope, loginService){});

var app = angular.module('app', ['sharedServices', 'login']);
當我們開發一個大型應用程式時,可能並不是所有東西都包含在一個頁面上。將同一類特性置於一個模組內,能使跨app間重用模組變得更容易。

3. 依賴注入

依賴注入是AngularJS最好的模式之一,它使得測試更為簡單,並且依賴任何指定物件都很明確。AngularJS的注入方式非常靈活,最簡單的方式只需要將依賴的名字傳入模組的function中即可:
var app = angular.module('app',[]);

app.controller('MainCtrl', function($scope, $timeout){
    $timeout(function(){
        console.log($scope);
    }, 1000);
});


這裡,很明顯,MainCtrl依賴$scope和$timeout。

直到你準備將其部署到生產環境並希望精簡程式碼時,一切都很美好。如果使用UglifyJS,之前的例子會變成下面這樣:
var app=angular.module("app",[]);
app.controller("MainCtrl",function(e,t){t(function(){console.log(e)},1e3)})
現在AngularJS怎麼知道MainCtrl依賴誰?AngularJS提供了一種非常簡單的解決方法,即將依賴作為一個陣列傳入,陣列的最後一個元素是一個函式,所有的依賴項作為它的引數。
app.controller('MainCtrl', ['$scope', '$timeout', function($scope, $timeout){
    $timeout(function(){
        console.log($scope);
    }, 1000);
}]);
這樣做能夠精簡程式碼,並且AngularJS知道如何解釋這些明確的依賴:
app.controller("MainCtrl",["$scope","$timeout",function(e,t){t(function(){consol

3.1 全域性依賴

在編寫AngularJS程式時,時常會出現這種情況:某個物件有一個依賴,而這個物件又將其自身繫結在全域性scope上,這意味著在任何AngularJS程式碼中這個依賴都是可用的,但這卻破壞了依賴注入模型,並會導致一些問題,尤其體現在測試過程中。

使用AngularJS可以很容易的將這些全域性依賴封裝進模組中,所以它們可以像AngularJS標準模組那樣被注入進去。

Underscrore.js是一個很讚的庫,它可以以函式式的風格簡化Javascript程式碼,通過以下方式,你可以將其轉化為一個模組:
var underscore = angular.module('underscore', []);
underscore.factory('_', function() {
  return window._; //Underscore must already be loaded on the page
});
var app = angular.module('app', ['underscore']);

app.controller('MainCtrl', ['$scope', '_', function($scope, _) {
    init = function() {
          _.keys($scope);
      }

      init();
}]);


這樣的做法允許應用程式繼續以AngularJS依賴注入的風格進行開發,同時在測試階段也能將underscore交換出去。

這可能看上去十分瑣碎,沒什麼必要,但如果你的程式碼中正在使用use strict(而且必須使用),那這就是必要的了。

4. 控制器膨脹

控制器是AngularJS的肉和土豆,一不小心就會將過多的邏輯加入其中,尤其是剛開始的時候。控制器永遠都不應該去操作DOM,或是持有DOM選擇器,那是我們需要使用指令和ng-model的地方。同樣的,業務邏輯應該存在於服務中,而非控制器。

資料也應該儲存在服務中,除非它們已經被繫結在$scope上了。服務本身是單例的,在應用程式的整個生命週期都存在,然而控制器在應用程式的各狀態間是瞬態的。如果資料被儲存在控制器中,當它被再次例項化時就需要重新從某處獲取資料。即使將資料儲存於localStorage中,檢索的速度也要比Javascript變數慢一個數量級。

AngularJS在遵循單一職責原則(SRP)時執行良好,如果控制器是檢視和模型間的協調者,那麼它所包含的邏輯就應該儘量少,這同樣會給測試帶來便利。

5. Service vs Factory

幾乎每一個AngularJS開發人員在初學時都會被這些名詞所困擾,這真的不太應該,因為它們就是針對幾乎相同事物的語法糖而已!

以下是它們在AngularJS原始碼中的定義:
function factory(name, factoryFn) {
    return provider(name, { $get: factoryFn });
}

function service(name, constructor) {
    return factory(name, ['$injector', function($injector) {
      return $injector.instantiate(constructor);
    }]);
}
從原始碼中你可以看到,service僅僅是呼叫了factory函式,而後者又呼叫了provider函式。事實上,AngularJS也為一些值、常量和裝飾提供額外的provider封裝,而這些並沒有導致類似的困惑,它們的文件都非常清晰。

由於service僅僅是呼叫了factory函式,這有什麼區別呢?線索在$injector.instantiate:在這個函式中,$injector在service的建構函式中建立了一個新的例項。

以下是一個例子,展示了一個service和一個factory如何完成相同的事情:
var app = angular.module('app',[]);

app.service('helloWorldService', function(){
    this.hello = function() {
        return "Hello World";
    };
});

app.factory('helloWorldFactory', function(){
    return {
        hello: function() {
            return "Hello World";
        }
    }
});
當helloWorldService或helloWorldFactory被注入到控制器中,它們都有一個hello方法,返回”hello world”。service的建構函式在宣告時被例項化了一次,同時factory物件在每一次被注入時傳遞,但是仍然只有一個factory例項。所有的providers都是單例。

既然能做相同的事,為什麼需要兩種不同的風格呢?相對於service,factory提供了更多的靈活性,因為它可以返回函式,這些函式之後可以被新建出來。這迎合了物件導向程式設計中工廠模式的概念,工廠可以是一個能夠建立其他物件的物件。
app.factory('helloFactory', function() {
    return function(name) {
        this.name = name;

        this.hello = function() {
            return "Hello " + this.name;
        };
    };
});
這裡是一個控制器示例,使用了service和兩個factory,helloFactory返回了一個函式,當新建物件時會設定name的值。
app.controller('helloCtrl', function($scope, helloWorldService, helloWorldFactory, helloFactory) {
    init = function() {
      helloWorldService.hello(); //'Hello World'
      helloWorldFactory.hello(); //'Hello World'
      new helloFactory('Readers').hello() //'Hello Readers'
    }

    init();
});

在初學時,最好只使用service。

Factory在設計一個包含很多私有方法的類時也很有用:
app.factory('privateFactory', function(){
    var privateFunc = function(name) {
        return name.split("").reverse().join(""); //reverses the name
    };

    return {
        hello: function(name){
          return "Hello " + privateFunc(name);
        }
    };
});


通過這個例子,我們可以讓privateFactory的公有API無法訪問到privateFunc方法,這種模式在service中是可以做到的,但在factory中更容易。

6. 沒有使用Batarang

Batarang是一個出色的Chrome外掛,用來開發和測試AngularJS app。

Batarang提供了瀏覽模型的能力,這使得我們有能力觀察AngularJS內部是如何確定繫結到作用域上的模型的,這在處理指令以及隔離一定範圍觀察繫結值時非常有用。

Batarang也提供了一個依賴圖, 如果我們正在接觸一個未經測試的程式碼庫,這個依賴圖就很有用,它能決定哪些服務應該被重點關照。

最後,Batarang提供了效能分析。Angular能做到開包即用,效能良好,然而對於一個充滿了自定義指令和複雜邏輯的應用而言,有時候就不那麼流暢了。使用Batarang效能工具,能夠直接觀察到在一個digest週期中哪個函式執行了最長時間。效能工具也能展示一棵完整的watch樹,在我們擁有很多watcher時,這很有用。

7. 過多的watcher

在上一點中我們提到,AngularJS能做到開包即用,效能良好。由於需要在一個digest週期中完成髒資料檢查,一旦watcher的數量增長到大約2000時,這個週期就會產生顯著的效能問題。(2000這個數字不能說一定會造成效能大幅下降,但這是一個不錯的經驗數值。在AngularJS 1.3 release版本中,已經有一些允許嚴格控制digest週期的改動了,Aaron Gray有一篇很好的文章對此進行解釋。)

以下這個“立即執行的函式表示式(IIFE)”會列印出當前頁面上所有的watcher的個數,你可以簡單的將其貼上到控制檯中,觀察結果。這段IIFE來源於Jared在StackOverflow上的回答:
(function () {
    var root = $(document.getElementsByTagName('body'));
    var watchers = [];

    var f = function (element) {
        if (element.data().hasOwnProperty('$scope')) {
            angular.forEach(element.data().$scope.$watchers, function (watcher) {
                watchers.push(watcher);
            });
        }

        angular.forEach(element.children(), function (childElement) {
            f($(childElement));
        });
    };

    f(root);

    console.log(watchers.length);
})();


通過這個方式得到watcher的數量,結合Batarang效能板塊中的watch樹,應該可以看到哪裡存在重複程式碼,或著哪裡存在不變資料同時擁有watch。

當存在不變資料,而你又想用AngularJS將其模版化,可以考慮使用bindonce。Bindonce是一個簡單的指令,允許你使用AngularJS中的模版,但它並不會加入watch,這就保證了watch數量不會增長。

8. 限定$scope的範圍

Javascript基於原型的繼承與物件導向中基於類的繼承有著微妙的區別,這通常不是什麼問題,但這個微妙之處在使用$scope時就會表現出來。在AngularJS中,每個$scope都會繼承父$scope,最高層稱之為$rootScope。($scope與傳統指令有些不同,它們有一定的作用範圍i,且只繼承顯式宣告的屬性。)

由於原型繼承的特點,在父類和子類間共享資料不太重要,不過如果不小心的話,也很容易誤用了一個父$scope的屬性。

比如說,我們需要在一個導航欄上顯示一個使用者名稱,這個使用者名稱是在登入表單中輸入的,下面這種嘗試應該是能工作的:
<div ng-controller="navCtrl">
   <span>{{user}}</span>
   <div ng-controller="loginCtrl">
        <span>{{user}}</span>
        <input ng-model="user"></input>
   </div>
</div>


那麼問題來了……:在text input中設定了user的ng-model,當使用者在其中輸入內容時,哪個模版會被更新?navCtrl還是loginCtrl,還是都會?

如果你選擇了loginCtrl,那麼你可能已經理解了原型繼承是如何工作的了。

當你檢索字面值時,原型鏈並不起作用。如果navCtrl也同時被更新的話,檢索原型鏈是必須的;但如果值是一個物件,這就會發生。(記住,在Javascript中,函式、陣列和物件都是物件)

所以為了獲得預期的行為,需要在navCtrl中建立一個物件,它可以被loginCtrl引用。
<div ng-controller="navCtrl">
   <span>{{user.name}}</span>
   <div ng-controller="loginCtrl">
        <span>{{user.name}}</span>
        <input ng-model="user.name"></input>
   </div>
</div>
現在,由於user是一個物件,原型鏈就會起作用,navCtrl模版和$scope和loginCtrl都會被更新。

這看上去是一個很做作的例子,但是當你使用某些指令去建立子$scope,如ngRepeat時,這個問題很容易就會產生。

9. 手工測試

由於TDD可能不是每個開發人員都喜歡的開發方式,因此當開發人員檢查程式碼是否工作或是否影響了其它東西時,他們會做手工測試。

不去測試AngularJS app,這是沒有道理的。AngularJS的設計使得它從頭到底都是可測試的,依賴注入和ngMock模組就是明證。AngularJS核心團隊已經開發了眾多能夠使測試更上一層樓的工具。

9.1 Protractor

單元測試是一個測試工作的基礎,但考慮到app的日益複雜,整合測試更貼近實際情況。幸運的是,AngularJS的核心團隊已經提供了必要的工具。

我們已經建立了Protractor,一個端到端的測試器用以模擬使用者互動,這能夠幫助你驗證你的AngularJS程式的健康狀況。

Protractor使用Jasmine測試框架定義測試,Protractor針對不同的頁面互動行為有一個非常健壯的API。

我們還有一些其他的端到端測試工具,但是Protractor的優勢是它能夠理解如何與AngularJS程式碼協同工作,尤其是在$digest週期中。

9.2 Karma

一旦我們用Protractor完成了整合測試的編寫工作,接下去就是執行測試了。等待測試執行,尤其是整合測試,對每個開發人員都是一種淡淡的憂傷。AngularJS的核心團隊也感到極為蛋疼,於是他們開發了Karma。

Karma是一個測試器,它有助於關閉反饋迴路。Karma之所以能夠做到這點,是因為它在指定檔案被改變時就執行測試。Karma同時也會在多個瀏覽器上執行測試,不同的裝置也可以指向Karma伺服器,這樣就能夠更好地覆蓋真實世界的應用場景。

10. 使用jQuery

jQuery是一個酷炫的庫,它有標準化的跨平臺開發,幾乎已經成為了現代化Web開發的必需品。不過儘管JQuery如此多的優秀特性,它的理念和AngularJS並不一致。

AngularJS是一個用來建立app的框架,而JQuery則是一個簡化“HTML文件操作、事件處理、動畫和Ajax”的庫。這是兩者最基本的區別,AngularJS致力於程式的體系結構,與HTML頁面無關。

為了更好的理解如何建立一個AngularJS程式,請停止使用jQuery。JQuery使開發人員以現存的HTML標準思考問題,但正如文件裡所說的,“AngularJS能夠讓你在應用程式中擴張HTML這個詞彙”。

DOM操作應該只在指令中完成,但這並不意味著他們只能用JQuery封裝。在你使用JQuery之前,你應該總是去想一下這個功能是不是AngularJS已經提供了。當指令互相依賴時能夠建立強大的工具,這確實很強大。

但一個非常棒的JQuery是必需品時,這一天可能會到來,但在一開始就引入它,是一個常見的錯誤。

結論

AngularJS是一卓越的框架,在社群的幫助下始終在進步。雖說AngularJS仍然是一個不斷髮展的概念,但我希望人們能夠遵循以上談到的這些約定,避免開發AngularJS應用所遇到的那些問題。
來自:前端裡
相關閱讀
評論(1)

相關文章