AngularJS開發(fā)者最常犯的10個(gè)錯(cuò)誤

2014-10-09 10:42:26來(lái)源:開源中國(guó)作者:

AngularJS是一個(gè)很不錯(cuò)的框架,并且和它的社區(qū)一起發(fā)展著。符合習(xí)慣的AngularJS仍舊是一個(gè)正在發(fā)展的概念,但希望以上這些對(duì)于規(guī)劃一個(gè)AngularJS應(yīng)用時(shí)會(huì)出現(xiàn)的陷阱希望可以被避免。

介紹

AngularJS是如今最受歡迎的JS框架之一,簡(jiǎn)化開發(fā)過(guò)程是它的目標(biāo)之一,這使得它非常適合于元型較小的apps的開發(fā),但也擴(kuò)展到具有全部特征的客戶端應(yīng)用的開發(fā)。易于開發(fā)、較多的特征及較好的效果導(dǎo)致了較多的應(yīng)用,伴隨而來(lái)的是一些陷阱。本文列舉了AngularJS的一些共同的易于也問(wèn)題的地方,尤其是在開發(fā)一個(gè)app的時(shí)候。

1. MVC目錄結(jié)構(gòu)

AngularJS是一個(gè)缺乏較好的term的MVC框架,其models不像backbone.js中那樣做為一個(gè)框架來(lái)定義,但其結(jié)構(gòu)模式仍匹配的較好。當(dāng)在一個(gè)MVC框架中作業(yè)時(shí),基于文件類型將文件組合在一起是其共同的要求:

templates/
    _login.html
    _feed.html
app/
    app.js
    controllers/
        LoginController.js
        FeedController.js
    directives/
        FeedEntryDirective.js
    services/
        LoginService.js
        FeedService.js
    filters/
        CapatalizeFilter.js

這樣的布局, 尤其是對(duì)那些有 Rails 背景的人來(lái)說(shuō), 看起來(lái)挺合理. 可是當(dāng) app 變得越來(lái)越龐大的時(shí)候, 這樣的布局結(jié)構(gòu)會(huì)導(dǎo)致每次都會(huì)打開一堆文件夾. 無(wú)論你是用 Sublime, Visual Studio, 還是 Vim with Nerd Tree, 每次都要花上很多時(shí)間滑動(dòng)滾動(dòng)條瀏覽這個(gè)目錄樹來(lái)查找文件.

如果我們根據(jù)每個(gè)文件隸屬的功能模塊來(lái)對(duì)文件分組, 而不是根據(jù)它隸屬的層:

app/
    app.js
    Feed/
        _feed.html
        FeedController.js
        FeedEntryDirective.js
        FeedService.js
    Login/
        _login.html
        LoginController.js
        LoginService.js
    Shared/
        CapatalizeFilter.js

那么查找某個(gè)功能模塊的文件就要容易得多, 自然可以提高開發(fā)的速度. 也許把 html 文件跟 js 文件放在混合放在一起做法不是每個(gè)人都能認(rèn)同. 但是起碼它省下寶貴的時(shí)間.

2、模塊分組

一開始就將主模塊中所有子模塊展示出來(lái)是通常的做法。但是開始做一個(gè)小應(yīng)用還好,但是做大了就不好管理了。

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

一個(gè)比較好的辦法是將相似類型的子模塊分組:

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']);

這個(gè)方法與上面那個(gè)方法效果差不多,但是也不很大。運(yùn)用要分組的思想將使工作更容易。

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']);

當(dāng)創(chuàng)建一個(gè)大的應(yīng)用時(shí),所有模塊可能不會(huì)放在一頁(yè)里,但是將模塊根據(jù)類型進(jìn)行分組將使模塊的重用能力更強(qiáng)。

3 依賴注入

依賴注入是AngularJS最棒的模式之一。它使測(cè)試變得更加方便,也讓它所依賴的對(duì)象變的更加清楚明白。AngularJS 對(duì)于注入是非常靈活的。一個(gè)最簡(jiǎn)單的方式只需要為模塊將依賴的名字傳入函數(shù)中:

var app = angular.module('app',[]);app.controller('MainCtrl', function($scope, $timeout){
    $timeout(function(){
        console.log($scope);
    }, 1000);});

這里,很清楚的是MainCtrl依賴于$scope和$timeout。

直到你準(zhǔn)備投入生產(chǎn)并壓縮你的代碼。使用UglifyJS,上面的例子會(huì)變成:

var app=angular.module("app",[]);
app.controller("MainCtrl",function(e,t){t(function(){console.log(e)},1e3)})

現(xiàn)在AngularJS怎么知道MainCtrl依賴什么?AngularJS提供了一個(gè)非常簡(jiǎn)單的解決方案:把依賴作為一個(gè)字符串?dāng)?shù)組傳遞,而數(shù)組的最后一個(gè)元素是一個(gè)把所有依賴作為參數(shù)的函數(shù)。

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

接下來(lái)在壓縮的代碼中AngularJS也可以知道如何找到依賴:

app.controller("MainCtrl",["$scope","$timeout",function(e,t){t(function(){console.log(e)},1e3)}])

3.1 全局依賴

通常在寫AngularJS應(yīng)用時(shí)會(huì)有一個(gè)對(duì)象作為依賴綁定到全局作用域中。這意味著它在任何AngularJS的代碼中都可用,但這打破了依賴注入模型同時(shí)帶來(lái)一些問(wèn)題,特別是在測(cè)試中。

AngularJS把這些全局變量封裝到模塊中,這樣它們可以像標(biāo)準(zhǔn)AngularJS模塊一樣被注入。

Underscore.js是很棒的庫(kù),它把Javascript代碼簡(jiǎn)化成了函數(shù)模式,并且它可以被轉(zhuǎn)化成一個(gè)模塊:

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

它允許應(yīng)用繼續(xù)用AngularJS依賴注入的風(fēng)格,也讓underscore在測(cè)試的時(shí)候被交換出來(lái)。

這或許看上去不重要,像是一個(gè)無(wú)關(guān)緊要的工作,但如果你的代碼正在使用use strict(應(yīng)該使用),那么這就變得有必要了。

4 控制器膨脹

控制器是AngularJS應(yīng)用中的肉和番茄。它很簡(jiǎn)單,特別是開始的時(shí)候,在控制器中放入過(guò)多的邏輯?刂破鞑粦(yīng)該做任何DOM操作或者有DOM選擇器,這應(yīng)該由使用ngModel的指令(directives)做的事。同樣地,業(yè)務(wù)邏輯應(yīng)該在服務(wù)(services)中,而不是 控制器。

數(shù)據(jù)也應(yīng)該被存在服務(wù)(services)中,除非它已經(jīng)和$scope關(guān)聯(lián)。服務(wù)(services)是留存于整個(gè)應(yīng)用生命周期的個(gè)體,同時(shí)控制器在應(yīng)用各階段間都是暫態(tài)的。如果數(shù)據(jù)被存在控制器中,那么當(dāng)它被重新實(shí)例化的時(shí)候,就需要從其他地方抓取。即使數(shù)據(jù)被存儲(chǔ)在localStorage中,獲取數(shù)據(jù)也要比從Javascript變量中獲取要慢幾個(gè)數(shù)量級(jí)。

AngularJS在遵從簡(jiǎn)單責(zé)任原則(SRP)時(shí)工作地最好。如果控制器是視圖和模型的協(xié)調(diào)者,那么它擁有的邏輯應(yīng)該被最小化。這將使得測(cè)試變的更加簡(jiǎn)單。 

5 Service 和 Factory的區(qū)別

幾乎每一個(gè)剛接觸AngularJS的開發(fā)者,都會(huì)對(duì)這兩個(gè)東西產(chǎn)生困惑。 雖然它們(幾乎)實(shí)現(xiàn)了同樣的效果,但真的不是語(yǔ)法糖。

這里是它們?cè)?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 函數(shù)只是調(diào)用 factory 函數(shù),然后 factory 函數(shù)再調(diào)用 provider 函數(shù)。事實(shí)上,value、constant和decorator 也是 AngularJS 提供的對(duì) provider 的封裝,但對(duì)它們使用場(chǎng)景不會(huì)有這種困惑,并且文檔描述也非常清晰。

那么Service 僅僅是單純的調(diào)用了一次 factory 函數(shù)嗎? 重點(diǎn)在 $injector.instantiate 中; 在這個(gè)函數(shù)里service會(huì)接收一個(gè)由$injector 使用new關(guān)鍵字去實(shí)例化的一個(gè)構(gòu)造器對(duì)象。(原文:with in this function $injector creates a new instance of the service's constructor function.)

下面是完成同樣功能的一個(gè)service和一個(gè)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";
        }
    }});

當(dāng) helloWorldService 或者 helloWorldFactory中的任何一個(gè)注入到controller里面, 他們都有一個(gè)返回字符串"Hello World"的名稱為 hello方法。 這個(gè)service 的構(gòu)造函數(shù)只在聲明時(shí)被實(shí)例化一次,并且在這個(gè) factory 對(duì)象每次被注入時(shí)各種互相引用, 但這個(gè) factory還是只是被實(shí)例化了一次。 所有的 providers 都是單例的。

既然都完成同樣的功能,為什么會(huì)有這兩種格式存在?factory比service略微更靈活一些,因?yàn)樗鼈兛梢允褂胣ew關(guān)鍵字返回函數(shù)(原文:Factories offer slightly more flexibility than services because they can return functions which can then be new'd)。 在其他地方,從面向?qū)ο缶幊痰墓S模式來(lái)說(shuō)。 一個(gè)factory可以是一個(gè)用于創(chuàng)建其他對(duì)象的對(duì)象。

app.factory('helloFactory', function() {
    return function(name) {
        this.name = name;
 
        this.hello = function() {
            return "Hello " + this.name;
        };
    };
});

這里有一個(gè)使用了前面提到的那個(gè)service和兩個(gè)factory的controller 的例子。需要注意的是 helloFactory 返回的是一個(gè)函數(shù),變量name的值是在對(duì)象使用new關(guān)鍵字的時(shí)候設(shè)置。

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

在剛?cè)腴T時(shí)候最好只使用services.

Factory更加適用于當(dāng)你在設(shè)計(jì)一個(gè)需要私有方法的類的時(shí)候使用:

app.factory('privateFactory', function(){
    var privateFunc = function(name) {
        return name.split("").reverse().join(""); //reverses the name
    };
 
    return {
        hello: function(name){
          return "Hello " + privateFunc(name);
        }
    };});

在這個(gè)例子中privateFactory含有一個(gè)不能被外部訪問(wèn)的私有privateFunc函數(shù)。這種使用方式services也可以實(shí)現(xiàn),但是使用Factory代碼結(jié)構(gòu)顯得更加清晰。

6 不會(huì)使用 Batarang

Batarang 是用于開發(fā)和調(diào)試 AngularJS 應(yīng)用的一個(gè)優(yōu)秀的chrome瀏覽器插件。

Batarang 提供了模型瀏覽,可以查看Angular內(nèi)部哪些模型已經(jīng)綁定到作用域(scopes )?梢杂糜谛枰谶\(yùn)行時(shí)查看指令中的隔離作用域(isolate scopes)綁定的值。

Batarang 還提供了依賴關(guān)系圖。 對(duì)于引入一個(gè)未測(cè)試的代碼庫(kù), 這個(gè)工具可以快速確定哪些services應(yīng)該得到更多的關(guān)注。

最后, Batarang提供了性能分析。 AngularJS 雖然是高性能開箱即用, 但是隨著應(yīng)用自定義指令和復(fù)雜的業(yè)務(wù)邏輯的增長(zhǎng),有時(shí)候會(huì)感到頁(yè)面不夠流暢。使用 Batarang 的性能分析工具可以很方便的查看哪些functions 在digest 周期中占用了更多的時(shí)間。這個(gè)工具還可以顯示出整個(gè)監(jiān)控樹(full watch tree),當(dāng)頁(yè)面有太多的監(jiān)控器(watch)時(shí),這個(gè)功能就顯得有用了。

7 太多的watchers

正如上文中提到的,在外部AngularJS是很不錯(cuò)的。因?yàn)樵谝粋(gè)循環(huán)消化中需要進(jìn)行dirty檢查,一旦watcher的數(shù)目超過(guò)2,000,循環(huán)會(huì)出現(xiàn)很明顯的問(wèn)題。(2,000僅是一個(gè)參考數(shù),在1.3版本中AngularJS對(duì)循環(huán)消化有更為嚴(yán)謹(jǐn)?shù)目刂疲P(guān)于這個(gè)Aaron Graye有較為詳細(xì)的敘述)

 這個(gè)IIFE(快速響應(yīng)函數(shù))可輸出當(dāng)前本頁(yè)中的watcher的數(shù)目,只需將其復(fù)制到console即可查看詳情。IIFE的來(lái)源跟Jared關(guān)于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);})();

使用這個(gè),可以從Batarang的效率方面來(lái)決定watcher及watch tree的數(shù)目,可以看到在哪些地方顧在或哪些地方?jīng)]有改變的數(shù)據(jù)有一個(gè)watch。

當(dāng)有數(shù)據(jù)沒有變化時(shí),但在Angular中又想讓它成為模板,可以考慮使用bindonce.Bindonce在Angular中僅是一個(gè)可能使用模板的指令,但沒有增加watch的數(shù)目。

8 審視$scope

Javascript的基于原型的繼承和基于類的繼承在一些細(xì)微的方面是不同的。通常這不是問(wèn)題,但是差別往往會(huì)在使用$scope時(shí)出現(xiàn)。在AngularJS中每一個(gè)$scope都從它的父$scope繼承過(guò)來(lái),最高層是$rootScope。($scope在指令中表現(xiàn)的有些不同,指令中的隔離作用域僅繼承那些顯式聲明的屬性。)

從父級(jí)那里分享數(shù)據(jù)對(duì)于原型繼承來(lái)說(shuō)并不重要。不過(guò)如果不小心的話,會(huì)遮蔽父級(jí)$scope的屬性。

我們想在導(dǎo)航欄上呈現(xiàn)一個(gè)用戶名,然后進(jìn)入登陸表單。

<div ng-controller="navCtrl">
   <span>{{user}}</span>
   <div ng-controller="loginCtrl">
        <span>{{user}}</span>
        <input ng-model="user"></input>
   </div></div>

考你下:當(dāng)用戶在設(shè)置了ngModel的文本框中輸入了值,哪個(gè)模板會(huì)被更新?是navCtrl,loginCtrl還是兩者?

如果你選loginCtrl,那么你可能對(duì)原型繼承的機(jī)理比較了解了。當(dāng)尋找字面值時(shí),原型鏈并沒有被涉及。如果navCtrl要被更新的話,那么查找原型鏈?zhǔn)潜匾。?dāng)一個(gè)值時(shí)對(duì)象的時(shí)候就會(huì)發(fā)生這些。(記住在Javascript中,函數(shù)、數(shù)組合對(duì)象都算作對(duì)象)

所以想要獲得期望的效果就需要在navCtrl上創(chuàng)建一個(gè)對(duì)象可以被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>

現(xiàn)在既然user是一個(gè)對(duì)象了,原型鏈會(huì)被考慮進(jìn)去,navCtrl的模板和$scope也會(huì)隨著loginCtrl更新。

這可能看上去像一個(gè)設(shè)計(jì)好的例子,但當(dāng)涉及到像ngRepeat那樣會(huì)創(chuàng)建子$scope的時(shí)候問(wèn)題就會(huì)出現(xiàn)。

9 手工測(cè)試

雖然測(cè)試驅(qū)動(dòng)開發(fā)可能不是每一個(gè)開發(fā)者都喜歡的開發(fā)方式,不過(guò)每次開發(fā)者去檢查他們的代碼是否工作或開始砸東西時(shí),他們正在做手工測(cè)試。
沒有理由不去測(cè)試一個(gè)AngularJS應(yīng)用。AngularJS從一開始就是被設(shè)計(jì)地易于測(cè)試的。依賴注入和ngMock模塊就是證據(jù)。核心團(tuán)隊(duì)開發(fā)了一些工具來(lái)講測(cè)試帶到另一個(gè)級(jí)別。

9.1 Protractor 

單元測(cè)試是一組測(cè)試集的基本元素,但隨著應(yīng)用復(fù)雜性的提高,集成測(cè)試會(huì)引出更多實(shí)際問(wèn)題。幸運(yùn)地是AngularJS核心團(tuán)隊(duì)提供了必要的工具。

“我們構(gòu)建了Protractor,一個(gè)端對(duì)端的測(cè)試運(yùn)行工具,模擬用戶交互,幫助你驗(yàn)證你的Angular應(yīng)用的運(yùn)行狀況。”

Protractor使用Jasmine測(cè)試框架來(lái)定義測(cè)試。Protractor為不同的頁(yè)面交互提供一套健壯的API。

有其他的端對(duì)端工具,不過(guò)Protractor有著自己的優(yōu)勢(shì),它知道怎么和AngularJS的代碼一起運(yùn)行,特別是面臨$digest循環(huán)的時(shí)候。 

9.2 Karma

一旦使用Protractor寫好了集成測(cè)試,測(cè)試需要被運(yùn)行起來(lái)。等待測(cè)試運(yùn)行特別是集成測(cè)試,會(huì)讓開發(fā)者感到沮喪。AngularJS核心團(tuán)隊(duì)也感到了這個(gè)痛苦并開發(fā)了Karma。

Karma是一個(gè)Javascript測(cè)試運(yùn)行工具,可以幫助你關(guān)閉反饋循環(huán)。Karma可以在特定的文件被修改時(shí)運(yùn)行測(cè)試,它也可以在不同的瀏覽器上并行測(cè)試。不同的設(shè)備可以指向Karma服務(wù)器來(lái)覆蓋實(shí)際場(chǎng)景。

10 jQuery的使用

jQuery 是個(gè)很不錯(cuò)的類庫(kù). 它將跨平臺(tái)開發(fā)標(biāo)準(zhǔn)化. 在現(xiàn)代網(wǎng)頁(yè)開發(fā)中具有很重要的地位. 雖然 jQuery 擁有許多強(qiáng)大的功能. 但是他的設(shè)計(jì)理念卻與 AngularJS 大相徑庭.

AngularJS 是用來(lái)開發(fā)應(yīng)用框架的; jQuery 則是一個(gè)用來(lái)簡(jiǎn)化 HTML 文檔對(duì)象遍歷和操作, 事件處理, 動(dòng)畫以及 Ajax 使用的類庫(kù)而已. 這是它們倆在本質(zhì)上的區(qū)別. AngularJS 側(cè)重點(diǎn)在于應(yīng)用的架構(gòu), 而非僅僅是補(bǔ)充 HTML 網(wǎng)頁(yè)的功能.

如文檔所述 AngularJS 可以讓你根據(jù)應(yīng)用的需要對(duì) HTML 進(jìn)一步擴(kuò)展. 所以, 如果想要深入的了解 AngularJS 應(yīng)用開發(fā), 就不應(yīng)該再繼續(xù)抱著 jQuery 的大腿. jQuery 只會(huì)把程序員的思維方式限制在現(xiàn)有的 HTML 標(biāo)準(zhǔn)里頭.

DOM操作應(yīng)該出現(xiàn)在指令中,但這并不意味著一定要使用jQuery包裝集。在使用jQuery前要考慮到一些功能AngularJS已經(jīng)提供了。指令建立于相互之間,并可以創(chuàng)建有用的工具。

總有一天,使用jQuery庫(kù)是必要的,不過(guò)從一開始就引入它無(wú)疑是一個(gè)錯(cuò)誤。

總結(jié)

AngularJS是一個(gè)很不錯(cuò)的框架,并且和它的社區(qū)一起發(fā)展著。符合習(xí)慣的AngularJS仍舊是一個(gè)正在發(fā)展的概念,但希望以上這些對(duì)于規(guī)劃一個(gè)AngularJS應(yīng)用時(shí)會(huì)出現(xiàn)的陷阱希望可以被避免。 

英文原文:The Top 10 Mistakes AngularJS Developers Make

關(guān)鍵詞:AngularJS