AngularJS是目前最为活跃的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使得AngularJS很是善于构建小型app原型,但AngularJS对于全功能的客户端应用程序一样强大,它结合了开发简便,特性普遍和出众的性能,使其被普遍使用。然而,大量使用也会产生诸多误区。如下这份列表摘取了常见的一些AngularJS的错误用法,尤为是在app开发过程当中。javascript
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
将全部东西都一股脑放在主模块下是很常见的,对于小型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)}])
在编写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(并且必须使用),那这就是必要的了。
控制器是AngularJS的肉和土豆,一不当心就会将过多的逻辑加入其中,尤为是刚开始的时候。控制器永远都不该该去操做DOM,或是持有DOM选择器,那是咱们须要使用指令和ng-model的地方。一样的,业务逻辑应该存在于服务中,而非控制器。
数据也应该存储在服务中,除非它们已经被绑定在$scope上了。服务自己是单例的,在应用程序的整个生命周期都存在,然而控制器在应用程序的各状态间是瞬态的。若是数据被保存在控制器中,当它被再次实例化时就须要从新从某处获取数据。即便将数据存储于localStorage中,检索的速度也要比Javascript变量慢一个数量级。
AngularJS在遵循单一职责原则(SRP)时运行良好,若是控制器是视图和模型间的协调者,那么它所包含的逻辑就应该尽可能少,这一样会给测试带来便利。
几乎每个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中更容易。
Batarang是一个出色的Chrome插件,用来开发和测试AngularJS app。
Batarang提供了浏览模型的能力,这使得咱们有能力观察AngularJS内部是如何肯定绑定到做用域上的模型的,这在处理指令以及隔离必定范围观察绑定值时很是有用。
Batarang也提供了一个依赖图, 若是咱们正在接触一个未经测试的代码库,这个依赖图就颇有用,它能决定哪些服务应该被重点关照。
最后,Batarang提供了性能分析。Angular能作到开包即用,性能良好,然而对于一个充满了自定义指令和复杂逻辑的应用而言,有时候就不那么流畅了。使用Batarang性能工具,可以直接观察到在一个digest周期中哪一个函数运行了最长时间。性能工具也能展现一棵完整的watch树,在咱们拥有不少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数量不会增加。
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时,这个问题很容易就会产生。
因为TDD可能不是每一个开发人员都喜欢的开发方式,所以当开发人员检查代码是否工做或是否影响了其它东西时,他们会作手工测试。
不去测试AngularJS app,这是没有道理的。AngularJS的设计使得它从头到底都是可测试的,依赖注入和ngMock模块就是明证。AngularJS核心团队已经开发了众多可以使测试更上一层楼的工具。
单元测试是一个测试工做的基础,但考虑到app的日益复杂,集成测试更贴近实际状况。幸运的是,AngularJS的核心团队已经提供了必要的工具。
咱们已经创建了Protractor,一个端到端的测试器用以模拟用户交互,这可以帮助你验证你的AngularJS程序的健康情况。
Protractor使用Jasmine测试框架定义测试,Protractor针对不一样的页面交互行为有一个很是健壮的API。
咱们还有一些其余的端到端测试工具,可是Protractor的优点是它可以理解如何与AngularJS代码协同工做,尤为是在$digest周期中。
一旦咱们用Protractor完成了集成测试的编写工做,接下去就是执行测试了。等待测试执行,尤为是集成测试,对每一个开发人员都是一种淡淡的忧伤。AngularJS的核心团队也感到极为蛋疼,因而他们开发了Karma。
Karma是一个测试器,它有助于关闭反馈回路。Karma之因此可以作到这点,是由于它在指定文件被改变时就运行测试。Karma同时也会在多个浏览器上运行测试,不一样的设备也能够指向Karma服务器,这样就可以更好地覆盖真实世界的应用场景。
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应用所遇到的那些问题。