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

2014-10-09 10:42:26來源:開源中國作者:

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

介紹

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

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

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

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 變得越來越龐大的時候, 這樣的布局結(jié)構(gòu)會導致每次都會打開一堆文件夾. 無論你是用 Sublime, Visual Studio, 還是 Vim with Nerd Tree, 每次都要花上很多時間滑動滾動條瀏覽這個目錄樹來查找文件.

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

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

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

2、模塊分組

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

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

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

3 依賴注入

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

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

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

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

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

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

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(){console.log(e)},1e3)}])

3.1 全局依賴

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

AngularJS把這些全局變量封裝到模塊中,這樣它們可以像標準AngularJS模塊一樣被注入。

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

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依賴注入的風格,也讓underscore在測試的時候被交換出來。

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

4 控制器膨脹

控制器是AngularJS應(yīng)用中的肉和番茄。它很簡單,特別是開始的時候,在控制器中放入過多的邏輯?刂破鞑粦(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)是留存于整個應(yīng)用生命周期的個體,同時控制器在應(yīng)用各階段間都是暫態(tài)的。如果數(shù)據(jù)被存在控制器中,那么當它被重新實例化的時候,就需要從其他地方抓取。即使數(shù)據(jù)被存儲在localStorage中,獲取數(shù)據(jù)也要比從Javascript變量中獲取要慢幾個數(shù)量級。

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

5 Service 和 Factory的區(qū)別

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

這里是它們在 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ù)。事實上,value、constant和decorator 也是 AngularJS 提供的對 provider 的封裝,但對它們使用場景不會有這種困惑,并且文檔描述也非常清晰。

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

下面是完成同樣功能的一個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中的任何一個注入到controller里面, 他們都有一個返回字符串"Hello World"的名稱為 hello方法。 這個service 的構(gòu)造函數(shù)只在聲明時被實例化一次,并且在這個 factory 對象每次被注入時各種互相引用, 但這個 factory還是只是被實例化了一次。 所有的 providers 都是單例的。

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

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

這里有一個使用了前面提到的那個service和兩個factory的controller 的例子。需要注意的是 helloFactory 返回的是一個函數(shù),變量name的值是在對象使用new關(guān)鍵字的時候設(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時候最好只使用services.

Factory更加適用于當你在設(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);
        }
    };});

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

6 不會使用 Batarang

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

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

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

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

7 太多的watchers

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

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

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

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

8 審視$scope

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

從父級那里分享數(shù)據(jù)對于原型繼承來說并不重要。不過如果不小心的話,會遮蔽父級$scope的屬性。

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

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

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

如果你選loginCtrl,那么你可能對原型繼承的機理比較了解了。當尋找字面值時,原型鏈并沒有被涉及。如果navCtrl要被更新的話,那么查找原型鏈是必要的。當一個值時對象的時候就會發(fā)生這些。(記住在Javascript中,函數(shù)、數(shù)組合對象都算作對象)

所以想要獲得期望的效果就需要在navCtrl上創(chuàng)建一個對象可以被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是一個對象了,原型鏈會被考慮進去,navCtrl的模板和$scope也會隨著loginCtrl更新。

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

9 手工測試

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

9.1 Protractor 

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

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

Protractor使用Jasmine測試框架來定義測試。Protractor為不同的頁面交互提供一套健壯的API。

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

9.2 Karma

一旦使用Protractor寫好了集成測試,測試需要被運行起來。等待測試運行特別是集成測試,會讓開發(fā)者感到沮喪。AngularJS核心團隊也感到了這個痛苦并開發(fā)了Karma。

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

10 jQuery的使用

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

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

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

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

總有一天,使用jQuery庫是必要的,不過從一開始就引入它無疑是一個錯誤。

總結(jié)

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

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

關(guān)鍵詞:AngularJS