AngularJS 開發中常犯的10個錯誤

簡介

AngularJS是目前最爲活躍的Javascript框架之一,AngularJS的目標之一是簡化開發過程,這使得AngularJS很是善於構建小型app原型,但AngularJS對於全功能的客戶端應用程序一樣強大,它結合了開發簡便,特性普遍和出衆的性能,使其被普遍使用。然而,大量使用也會產生諸多誤區。如下這份列表摘取了常見的一些AngularJS的錯誤用法,尤爲是在app開發過程當中。javascript

1. MVC目錄結構

AngularJS,直白地說,就是一個MVC框架。它的模型並無像backbone.js框架那樣定義的如此明確,但它的體系結構卻恰如其分。當你工做於一個MVC框架時,廣泛的作法是根據文件類型對其進行歸類:html

 1 templates/
 2     _login.html
 3     _feed.html
 4 app/
 5     app.js
 6     controllers/
 7         LoginController.js
 8         FeedController.js
 9     directives/
10         FeedEntryDirective.js
11     services/
12         LoginService.js
13         FeedService.js
14     filters/
15         CapatalizeFilter.js

看起來,這彷佛是一個顯而易見的結構,更況且Rails也是這麼幹的。然而一旦app規模開始擴張,這種結構會致使你一次須要打開不少目錄,不管你是使用sublime,Visual Studio或是Vim結合Nerd Tree,你都會投入不少時間在目錄樹中不斷地滑上滑下。java

與按照類型劃分文件不一樣,取而代之的,咱們能夠按照特性劃分文件:git

 1 app/
 2     app.js
 3     Feed/
 4         _feed.html
 5         FeedController.js
 6         FeedEntryDirective.js
 7         FeedService.js
 8     Login/
 9         _login.html
10         LoginController.js
11         LoginService.js
12     Shared/
13         CapatalizeFilter.js

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

2. 模塊

將全部東西都一股腦放在主模塊下是很常見的,對於小型app,剛開始並無什麼問題,然而很快你就會發現坑爹的事來了。github

1 var app = angular.module('app',[]);
2 app.service('MyService', function(){
3     //service code
4 });
5 app.controller('MyCtrl', function($scope, MyService){
6     //controller code
7 });

在此以後,一個常見的策略是對相同類型的對象歸類。web

1 var services = angular.module('services',[]);
2 services.service('MyService', function(){
3     //service code
4 });
5 var controllers = angular.module('controllers',['services']);
6 controllers.controller('MyCtrl', function($scope, MyService){
7     //controller code
8 });
9 var app = angular.module('app',['controllers', 'services']);

這種方式和前面第一部分所談到的目錄結構差很少:不夠好。根據相同的理念,能夠按照特性歸類,這會帶來可擴展性。chrome

1 var sharedServicesModule = angular.module('sharedServices',[]);
2 sharedServices.service('NetworkService', function($http){});
3 var loginModule = angular.module('login',['sharedServices']);
4 loginModule.service('loginService', function(NetworkService){});
5 loginModule.controller('loginCtrl', function($scope, loginService){});
6 var app = angular.module('app', ['sharedServices', 'login']);

當咱們開發一個大型應用程序時,可能並非全部東西都包含在一個頁面上。將同一類特性置於一個模塊內,能使跨app間重用模塊變得更容易。編程

3. 依賴注入數組

依賴注入是AngularJS最好的模式之一,它使得測試更爲簡單,而且依賴任何指定對象都很明確。AngularJS的注入方式很是靈活,最簡單的方式只須要將依賴的名字傳入模塊的function中便可:

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

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

直到你準備將其部署到生產環境並但願精簡代碼時,一切都很美好。若是使用UglifyJS,以前的例子會變成下面這樣:

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

如今AngularJS怎麼知道MainCtrl依賴誰?AngularJS提供了一種很是簡單的解決方法,即將依賴做爲一個數組傳入,數組的最後一個元素是一個函數,全部的依賴項做爲它的參數。

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

這樣作可以精簡代碼,而且AngularJS知道如何解釋這些明確的依賴:

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

3.1 全局依賴

在編寫AngularJS程序時,時常會出現這種狀況:某個對象有一個依賴,而這個對象又將其自身綁定在全局scope上,這意味着在任何AngularJS代碼中這個依賴都是可用的,但這卻破壞了依賴注入模型,並會致使一些問題,尤爲體如今測試過程當中。

使用AngularJS能夠很容易的將這些全局依賴封裝進模塊中,因此它們能夠像AngularJS標準模塊那樣被注入進去。

Underscrore.js是一個很讚的庫,它能夠以函數式的風格簡化Javascript代碼,經過如下方式,你能夠將其轉化爲一個模塊:

 1 var underscore = angular.module('underscore', []);
 2 underscore.factory('_', function() {
 3   return window._; //Underscore must already be loaded on the page
 4 });
 5 var app = angular.module('app', ['underscore']);
 6  
 7 app.controller('MainCtrl', ['$scope', '_', function($scope, _) {
 8     init = function() {
 9           _.keys($scope);
10       }
11  
12       init();
13 }]);

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

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

4. 控制器膨脹

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

數據也應該存儲在服務中,除非它們已經被綁定在$scope上了。服務自己是單例的,在應用程序的整個生命週期都存在,然而控制器在應用程序的各狀態間是瞬態的。若是數據被保存在控制器中,當它被再次實例化時就須要從新從某處獲取數據。即便將數據存儲於localStorage中,檢索的速度也要比Javascript變量慢一個數量級。

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

5. Service vs Factory

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

如下是它們在AngularJS源代碼中的定義:

1 function factory(name, factoryFn) {
2     return provider(name, { $get: factoryFn });
3 }
4  
5 function service(name, constructor) {
6     return factory(name, ['$injector', function($injector) {
7       return $injector.instantiate(constructor);
8     }]);
9 }

從源代碼中你能夠看到,service僅僅是調用了factory函數,然後者又調用了provider函數。事實上,AngularJS也爲一些值、常量和裝飾提供額外的provider封裝,而這些並無致使相似的困惑,它們的文檔都很是清晰。

因爲service僅僅是調用了factory函數,這有什麼區別呢?線索在$injector.instantiate:在這個函數中,$injector在service的構造函數中建立了一個新的實例。

如下是一個例子,展現了一個service和一個factory如何完成相同的事情:

 1 var app = angular.module('app',[]);
 2  
 3 app.service('helloWorldService', function(){
 4     this.hello = function() {
 5         return "Hello World";
 6     };
 7 });
 8  
 9 app.factory('helloWorldFactory', function(){
10     return {
11         hello: function() {
12             return "Hello World";
13         }
14     }
15 });

當helloWorldService或helloWorldFactory被注入到控制器中,它們都有一個hello方法,返回」hello world」。service的構造函數在聲明時被實例化了一次,同時factory對象在每一次被注入時傳遞,可是仍然只有一個factory實例。全部的providers都是單例。

既然能作相同的事,爲何須要兩種不一樣的風格呢?相對於service,factory提供了更多的靈活性,由於它能夠返回函數,這些函數以後能夠被新建出來。這迎合了面向對象編程中工廠模式的概念,工廠能夠是一個可以建立其餘對象的對象。

1 app.factory('helloFactory', function() {
2     return function(name) {
3         this.name = name;
4  
5         this.hello = function() {
6             return "Hello " + this.name;
7         };
8     };
9 });

這裏是一個控制器示例,使用了service和兩個factory,helloFactory返回了一個函數,當新建對象時會設置name的值。

1 app.controller('helloCtrl', function($scope, helloWorldService, helloWorldFactory, helloFactory) {
2     init = function() {
3       helloWorldService.hello(); //'Hello World'
4       helloWorldFactory.hello(); //'Hello World'
5       new helloFactory('Readers').hello() //'Hello Readers'
6     }
7  
8     init();
9 });

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

Factory在設計一個包含不少私有方法的類時也頗有用:

 1 app.factory('privateFactory', function(){
 2     var privateFunc = function(name) {
 3         return name.split("").reverse().join(""); //reverses the name
 4     };
 5  
 6     return {
 7         hello: function(name){
 8           return "Hello " + privateFunc(name);
 9         }
10     };
11 });

經過這個例子,咱們可讓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上的回答:

 1 (function () {
 2     var root = $(document.getElementsByTagName('body'));
 3     var watchers = [];
 4  
 5     var f = function (element) {
 6         if (element.data().hasOwnProperty('$scope')) {
 7             angular.forEach(element.data().$scope.$$watchers, function (watcher) {
 8                 watchers.push(watcher);
 9             });
10         }
11  
12         angular.forEach(element.children(), function (childElement) {
13             f($(childElement));
14         });
15     };
16  
17     f(root);
18  
19     console.log(watchers.length);
20 })();

經過這個方式獲得watcher的數量,結合Batarang性能板塊中的watch樹,應該能夠看到哪裏存在重複代碼,或着哪裏存在不變數據同時擁有watch。

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

8. 限定$scope的範圍

Javascript基於原型的繼承與面向對象中基於類的繼承有着微妙的區別,這一般不是什麼問題,但這個微妙之處在使用$scope時就會表現出來。在AngularJS中,每一個$scope都會繼承父$scope,最高層稱之爲$rootScope。($scope與傳統指令有些不一樣,它們有必定的做用範圍i,且只繼承顯式聲明的屬性。)

因爲原型繼承的特色,在父類和子類間共享數據不過重要,不過若是不當心的話,也很容易誤用了一個父$scope的屬性。

好比說,咱們須要在一個導航欄上顯示一個用戶名,這個用戶名是在登陸表單中輸入的,下面這種嘗試應該是能工做的:

1 <div ng-controller="navCtrl">
2    <span>{{user}}</span>
3    <div ng-controller="loginCtrl">
4         <span>{{user}}</span>
5         <input ng-model="user"></input>
6    </div>
7 </div>

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

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

當你檢索字面值時,原型鏈並不起做用。若是navCtrl也同時被更新的話,檢索原型鏈是必須的;但若是值是一個對象,這就會發生。(記住,在Javascript中,函數、數組和對象都是對象)

因此爲了得到預期的行爲,須要在navCtrl中建立一個對象,它能夠被loginCtrl引用。

1 <div ng-controller="navCtrl">
2    <span>{{user.name}}</span>
3    <div ng-controller="loginCtrl">
4         <span>{{user.name}}</span>
5         <input ng-model="user.name"></input>
6    </div>
7 </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應用所遇到的那些問題。

相關文章
相關標籤/搜索