前言
AngularJS易于开发、较多的特征及较好的效果导致了较多的应用,伴随而来的是一些陷阱。本文列举了AngularJS的一些共同的易于出问题的地方,下面来一起看看吧。
一、MVC目录结构
AngularJS,直白地说,就是一个MVC框架。它的模型并没有像backbone.js框架那样定义的如此明确,但它的体系结构却恰如其分。当你工作于一个MVC框架时,普遍的做法是根据文件类型对其进行归类:
看起来,这似乎是一个显而易见的结构,更何况Rails也是这么干的。然而一旦app规模开始扩张,这种结构会导致你一次需要打开很多目录,无论你是使用sublime,Visual Studio或是Vim结合Nerd Tree,你都会投入很多时间在目录树中不断地滑上滑下。
与按照类型划分文件不同,取而代之的,我们可以按照特性划分文件:
这种目录结构使得我们能够更容易地找到与某个特性相关的所有文件,继而加快我们的开发进度。尽管将.html和.js文件置于一处可能存在争议,但节省下来的时间更有价值。
二、模块
将所有东西都一股脑放在主模块下是很常见的,对于小型app,刚开始并没有什么问题,然而很快你就会发现坑爹的事来了。
在此之后,一个常见的策略是对相同类型的对象归类。
controllers.controller('MyCtrl',MyService){
//controller code
});
var app = angular.module('app',['controllers','services']);
这种方式和前面第一部分所谈到的目录结构差不多:不够好。根据相同的理念,可以按照特性归类,这会带来可扩展性。
loginModule.service('loginService',function(NetworkService){});
loginModule.controller('loginCtrl',loginService){});
var app = angular.module('app',['sharedServices','login']);
当我们开发一个大型应用程序时,可能并不是所有东西都包含在一个页面上。将同一类特性置于一个模块内,能使跨app间重用模块变得更容易。
三、依赖注入
依赖注入是AngularJS最好的模式之一,它使得测试更为简单,并且依赖任何指定对象都很明确。AngularJS的注入方式非常灵活,最简单的方式只需要将依赖的名字传入模块的function
中即可:
$timeout(function(){
console.log($scope);
},1000);
});
这里,很明显,MainCtrl依赖$scope
和$timeout
。
直到你准备将其部署到生产环境并希望精简代码时,一切都很美好。如果使用UglifyJS,之前的例子会变成下面这样:
现在AngularJS怎么知道MainCtrl依赖谁?AngularJS提供了一种非常简单的解决方法,即将依赖作为一个数组传入,数组的最后一个元素是一个函数,所有的依赖项作为它的参数。
这样做能够精简代码,并且AngularJS知道如何解释这些明确的依赖:
3.1 全局依赖
在编写AngularJS程序时,时常会出现这种情况:某个对象有一个依赖,而这个对象又将其自身绑定在全局scope上,这意味着在任何AngularJS代码中这个依赖都是可用的,但这却破坏了依赖注入模型,并会导致一些问题,尤其体现在测试过程中。
使用AngularJS可以很容易的将这些全局依赖封装进模块中,所以它们可以像AngularJS标准模块那样被注入进去。
Underscrore.js是一个很赞的库,它可以以函数式的风格简化Javascript代码,通过以下方式,你可以将其转化为一个模块:
app.controller('MainCtrl','',) {
init = function() {
_.keys($scope);
}
init();
}]);
这样的做法允许应用程序继续以AngularJS依赖注入的风格进行开发,同时在测试阶段也能将underscore交换出去。
这可能看上去十分琐碎,没什么必要,但如果你的代码中正在使用use strict(而且必须使用),那这就是必要的了。
四、控制器膨胀
控制器是AngularJS的肉和土豆,一不小心就会将过多的逻辑加入其中,尤其是刚开始的时候。控制器永远都不应该去操作DOM,或是持有DOM选择器,那是我们需要使用指令和ng-model的地方。同样的,业务逻辑应该存在于服务中,而非控制器。
数据也应该存储在服务中,除非它们已经被绑定在$scope上了。服务本身是单例的,在应用程序的整个生命周期都存在,然而控制器在应用程序的各状态间是瞬态的。如果数据被保存在控制器中,当它被再次实例化时就需要重新从某处获取数据。即使将数据存储于localStorage中,检索的速度也要比Javascript变量慢一个数量级。
AngularJS在遵循单一职责原则(SRP)时运行良好,如果控制器是视图和模型间的协调者,那么它所包含的逻辑就应该尽量少,这同样会给测试带来便利。
五、Service vs Factory
几乎每一个AngularJS开发人员在初学时都会被这些名词所困扰,这真的不太应该,因为它们就是针对几乎相同事物的语法糖而已!
以下是它们在AngularJS源代码中的定义:
return factory(name,['$injector',function($injector) {
return $injector.instantiate(constructor);
}]);
}
从源代码中你可以看到,service仅仅是调用了factory
函数,而后者又调用了provider
函数。事实上,AngularJS也为一些值、常量和装饰提供额外的provider
封装,而这些并没有导致类似的困惑,它们的文档都非常清晰。
由于service仅仅是调用了factory
函数,这有什么区别呢?线索在$injector.instantiate
:在这个函数中,$injector
在service
的构造函数中创建了一个新的实例。
以下是一个例子,展示了一个service
和一个factory
如何完成相同的事情:
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
提供了更多的灵活性,因为它可以返回函数,这些函数之后可以被新建出来。这迎合了面向对象编程中工厂模式的概念,工厂可以是一个能够创建其他对象的对象。
return "Hello " + this.name;
};
};
});
这里是一个控制器示例,使用了service
和两个factory
,helloFactory
返回了一个函数,当新建对象时会设置name
的值。
});
在初学时,最好只使用service。
Factory
在设计一个包含很多私有方法的类时也很有用:
hello: function(name){
return "Hello " + privateFunc(name);
}
};
});
通过这个例子,我们可以让privateFactory
的公有API无法访问到privateFunc
方法,这种模式在service
中是可以做到的,但在factory
中更容易。
六、没有使用Batarang
Batarang是一个出色的Chrome插件,用来开发和测试AngularJS app。
Batarang提供了浏览模型的能力,这使得我们有能力观察AngularJS内部是如何确定绑定到作用域上的模型的,这在处理指令以及隔离一定范围观察绑定值时非常有用。
Batarang也提供了一个依赖图, 如果我们正在接触一个未经测试的代码库,这个依赖图就很有用,它能决定哪些服务应该被重点关照。
最后,Batarang提供了性能分析。Angular能做到开包即用,性能良好,然而对于一个充满了自定义指令和复杂逻辑的应用而言,有时候就不那么流畅了。使用Batarang性能工具,能够直接观察到在一个digest周期中哪个函数运行了最长时间。性能工具也能展示一棵完整的watch树,在我们拥有很多watcher时,这很有用。
七、过多的watcher
在上一点中我们提到,AngularJS能做到开包即用,性能良好。由于需要在一个digest周期中完成脏数据检查,一旦watcher的数量增长到大约2000时,这个周期就会产生显著的性能问题。(2000这个数字不能说一定会造成性能大幅下降,但这是一个不错的经验数值。在AngularJS 1.3 release版本中,已经有一些允许严格控制digest周期的改动了。)
以下这个“立即执行的函数表达式(IIFE)”会打印出当前页面上所有的watcher的个数,你可以简单的将其粘贴到控制台中,观察结果。这段IIFE来源于Jared在StackOverflow上的回答:
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数量不会增长。
八、限定$scope的范围
Javascript基于原型的继承与面向对象中基于类的继承有着微妙的区别,这通常不是什么问题,但这个微妙之处在使用$scope
时就会表现出来。在AngularJS中,每个$scope
都会继承父$scope
,最高层称之为$rootScope
。($scope
与传统指令有些不同,它们有一定的作用范围i,且只继承显式声明的属性。)
由于原型继承的特点,在父类和子类间共享数据不太重要,不过如果不小心的话,也很容易误用了一个父$scope
的属性。
比如说,我们需要在一个导航栏上显示一个用户名,这个用户名是在登录表单中输入的,下面这种尝试应该是能工作的: