总结AngularJS开发者最常犯的十个错误

前言

AngularJS易于开发、较多的特征及较好的效果导致了较多的应用,伴随而来的是一些陷阱。本文列举了AngularJS的一些共同的易于出问题的地方,下面来一起看看吧。

一、MVC目录结构

AngularJS,直白地说,就是一个MVC框架。它的模型并没有像backbone.js框架那样定义的如此明确,但它的体系结构却恰如其分。当你工作于一个MVC框架时,普遍的做法是根据文件类型对其进行归类:

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规模开始扩张,这种结构会导致你一次需要打开很多目录,无论你是使用sublime,Visual Studio或是Vim结合Nerd Tree,你都会投入很多时间在目录树中不断地滑上滑下。

与按照类型划分文件不同,取而代之的,我们可以按照特性划分文件:

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

这种目录结构使得我们能够更容易地找到与某个特性相关的所有文件,继而加快我们的开发进度。尽管将.html和.js文件置于一处可能存在争议,但节省下来的时间更有价值。

二、模块

将所有东西都一股脑放在主模块下是很常见的,对于小型app,刚开始并没有什么问题,然而很快你就会发现坑爹的事来了。

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

当我们开发一个大型应用程序时,可能并不是所有东西都包含在一个页面上。将同一类特性置于一个模块内,能使跨app间重用模块变得更容易。

三、依赖注入

依赖注入是AngularJS最好的模式之一,它使得测试更为简单,并且依赖任何指定对象都很明确。AngularJS的注入方式非常灵活,最简单的方式只需要将依赖的名字传入模块的function中即可:

var app = angular.module('app',[]);

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

这里,很明显,MainCtrl依赖$scope$timeout

直到你准备将其部署到生产环境并希望精简代码时,一切都很美好。如果使用UglifyJS,之前的例子会变成下面这样:

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

现在AngularJS怎么知道MainCtrl依赖谁?AngularJS提供了一种非常简单的解决方法,即将依赖作为一个数组传入,数组的最后一个元素是一个函数,所有的依赖项作为它的参数。

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程序时,时常会出现这种情况:某个对象有一个依赖,而这个对象又将其自身绑定在全局scope上,这意味着在任何AngularJS代码中这个依赖都是可用的,但这却破坏了依赖注入模型,并会导致一些问题,尤其体现在测试过程中。

使用AngularJS可以很容易的将这些全局依赖封装进模块中,所以它们可以像AngularJS标准模块那样被注入进去。

Underscrore.js是一个很赞的库,它可以以函数式的风格简化Javascript代码,通过以下方式,你可以将其转化为一个模块:

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

这样的做法允许应用程序继续以AngularJS依赖注入的风格进行开发,同时在测试阶段也能将underscore交换出去。

这可能看上去十分琐碎,没什么必要,但如果你的代码中正在使用use strict(而且必须使用),那这就是必要的了。

四、控制器膨胀

控制器是AngularJS的肉和土豆,一不小心就会将过多的逻辑加入其中,尤其是刚开始的时候。控制器永远都不应该去操作DOM,或是持有DOM选择器,那是我们需要使用指令和ng-model的地方。同样的,业务逻辑应该存在于服务中,而非控制器。

数据也应该存储在服务中,除非它们已经被绑定在$scope上了。服务本身是单例的,在应用程序的整个生命周期都存在,然而控制器在应用程序的各状态间是瞬态的。如果数据被保存在控制器中,当它被再次实例化时就需要重新从某处获取数据。即使将数据存储于localStorage中,检索的速度也要比Javascript变量慢一个数量级。

AngularJS在遵循单一职责原则(SRP)时运行良好,如果控制器是视图和模型间的协调者,那么它所包含的逻辑就应该尽量少,这同样会给测试带来便利。

五、Service vs Factory

几乎每一个AngularJS开发人员在初学时都会被这些名词所困扰,这真的不太应该,因为它们就是针对几乎相同事物的语法糖而已!

以下是它们在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仅仅是调用了factory函数,而后者又调用了provider函数。事实上,AngularJS也为一些值、常量和装饰提供额外的provider封装,而这些并没有导致类似的困惑,它们的文档都非常清晰。

由于service仅仅是调用了factory函数,这有什么区别呢?线索在$injector.instantiate:在这个函数中,$injectorservice的构造函数中创建了一个新的实例。

以下是一个例子,展示了一个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";
 }
 }
});

helloWorldServicehelloWorldFactory被注入到控制器中,它们都有一个hello方法,返回”hello world”。service的构造函数在声明时被实例化了一次,同时factory对象在每一次被注入时传递,但是仍然只有一个factory实例。所有的providers都是单例。

既然能做相同的事,为什么需要两种不同的风格呢?相对于servicefactory提供了更多的灵活性,因为它可以返回函数,这些函数之后可以被新建出来。这迎合了面向对象编程中工厂模式的概念,工厂可以是一个能够创建其他对象的对象。

app.factory('helloFactory', function() {
 return function(name) {
 this.name = name;

 this.hello = function() {
  return "Hello " + this.name;
 };
 };
});

这里是一个控制器示例,使用了service和两个factoryhelloFactory返回了一个函数,当新建对象时会设置name的值。

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

在初学时,最好只使用service。

Factory在设计一个包含很多私有方法的类时也很有用:

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的公有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上的回答:

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

通过这个方式得到watcher的数量,结合Batarang性能板块中的watch树,应该可以看到哪里存在重复代码,或着哪里存在不变数据同时拥有watch。

当存在不变数据,而你又想用AngularJS将其模版化,可以考虑使用bindonce。Bindonce是一个简单的指令,允许你使用AngularJS中的模版,但它并不会加入watch,这就保证了watch数量不会增长。

八、限定$scope的范围

Javascript基于原型的继承与面向对象中基于类的继承有着微妙的区别,这通常不是什么问题,但这个微妙之处在使用$scope时就会表现出来。在AngularJS中,每个$scope都会继承父$scope,最高层称之为$rootScope。($scope与传统指令有些不同,它们有一定的作用范围i,且只继承显式声明的属性。)

由于原型继承的特点,在父类和子类间共享数据不太重要,不过如果不小心的话,也很容易误用了一个父$scope的属性。

比如说,我们需要在一个导航栏上显示一个用户名,这个用户名是在登录表单中输入的,下面这种尝试应该是能工作的:

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

那么问题来了……:在text input中设置了user的ng-model,当用户在其中输入内容时,哪个模版会被更新?navCtrl还是loginCtrl,还是都会?

如果你选择了loginCtrl,那么你可能已经理解了原型继承是如何工作的了。

当你检索字面值时,原型链并不起作用。如果navCtrl也同时被更新的话,检索原型链是必须的;但如果值是一个对象,这就会发生。(记住,在Javascript中,函数、数组和对象都是对象)

所以为了获得预期的行为,需要在navCtrl中创建一个对象,它可以被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>

现在,由于user是一个对象,原型链就会起作用,navCtrl模版和$scopeloginCtrl都会被更新。

这看上去是一个很做作的例子,但是当你使用某些指令去创建子$scope,如ngRepeat时,这个问题很容易就会产生。

九、手工测试

由于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服务器,这样就能够更好地覆盖真实世界的应用场景。

十、使用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应用所遇到的那些问题。希望这篇文章的内容对大家能有有所帮助,如果有问题可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • JS提示:Uncaught SyntaxError:Unexpected token ) 错误的解决方法

    本文实例讲述了JS提示:Uncaught SyntaxError:Unexpected token ) 错误的解决方法.分享给大家供大家参考,具体如下: Uncaught SyntaxError: Unexpected token ) 以下代码引发了此异常: <div class="Hd_live_Sharediv left"> <a href="javascript:void()" onclick="loadLivePlayer('ud

  • JavaScript中日常收集常见的10种错误(推荐)

     1 对于this关键词的不正确使用 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout (function() { this.clearBoard(); }, 0); }; 运行上面的代码将会出现如下错误: uncaught typeError:undefined is not a function 为什么会有这个错? this是指代当前对象本身,this的调用和它所在

  • 微信小程序 加载 app-service.js 错误解决方法

    微信小程序开发过程中出现错误,经过一番思考解决此问题,这里给大家提醒下! 加载 app-service.js 错误 WAService.js:2 不要在 undefined.js 注册多个Page APP-SERVICE-Engine:Please do not register multiple Page in undefined.js 开始新建一个小程序,结果报这么多错误. 但是我点击到编辑页查看的时候,没有pages,utils呀.. 进入app.js没有注册多个page呀. 感觉很奇怪,

  • JSON 对象未定义错误的解决方法

    今天在使用Moodle的时候, 突然发现在IE下进入课件的时候提示"JSON 未定义"错误信息. 后来找来一个 "json2.js" 引入测试了一下. OK 解决. 后来一向觉的不靠谱,仔细推敲了一下发现我的IE是在兼容模式下. 换到正常模式, 注销到引用"json2.js"的代码. 果然是还是正常的了. 所以, 猜想IE可能是为了兼容模式中的"兼容"功能. 所以不会去加载过多不确定的东西(最小启动单元). 包括JSON的解析

  • Ajax向后台传json格式的数据出现415错误的原因分析及解决方法

    问题描述: ajax往后台传json格式数据报415错误,如下图所示 页面代码 function saveUser(){ var uuId = document.getElementById("uuid").value; var idCard = document.getElementById("idCard").value; alert(uuId+idCard); // var result = new Object(); // result.uuId = uuI

  • 防止Node.js中错误导致进程阻塞的办法

    前言 在Node.js中,当某个回调函数发生了错误,整个进程都会崩溃,影响后面的代码执行. Node.js这样处理,是因为在发生未被捕获的错误时,进程的状态就不确定.之后也就无法正常工作了.如果错误始终不处理的话,就回一直抛出意料之外的错误,这样不利于调试. 防止错误导致的进程阻塞的方法主要有如下两种: 一. try-catch try-catch允许进行异常捕获,并让代码继续执行下去: 例如: 当函数抛出错误时,代码就停止执行了: (function() { var a = 0; a(); c

  • 总结AngularJS开发者最常犯的十个错误

    前言 AngularJS易于开发.较多的特征及较好的效果导致了较多的应用,伴随而来的是一些陷阱.本文列举了AngularJS的一些共同的易于出问题的地方,下面来一起看看吧. 一.MVC目录结构 AngularJS,直白地说,就是一个MVC框架.它的模型并没有像backbone.js框架那样定义的如此明确,但它的体系结构却恰如其分.当你工作于一个MVC框架时,普遍的做法是根据文件类型对其进行归类: templates/ _login.html _feed.html app/ app.js cont

  • Python程序员开发中常犯的10个错误

    Python是一门简单易学的编程语言,语法简洁而清晰,并且拥有丰富和强大的类库.与其它大多数程序设计语言使用大括号不一样 ,它使用缩进来定义语句块. 在平时的工作中,Python开发者很容易犯一些小错误,这些错误都很容易避免,本文总结了Python开发者最常犯的10个错误,一起来看下,不知你中枪了没有. 1.滥用表达式作为函数参数默认值 Python允许开发者指定一个默认值给函数参数,虽然这是该语言的一个特征,但当参数可变时,很容易导致混乱,例如,下面这段函数定义: 复制代码 代码如下: >>

  • C#程序员最易犯的编程错误

    本文介绍了10种最常见的编程错误,或是C#程序员要避免的陷阱. 常见错误1: 像使用值一样使用参考或过来用 C++以及许多其他语言的程序员习惯于控制他们分配给变量的值是否为简易的值或现有对象的引用.在C#中呢,这将由写该对象的程序员决定,而不是由实例化该对象并对它进行变量赋值的程序员决定.这是新手C#程序员们的共同"问题". 如果你不知道你正在使用的对象是否是值类型或引用类型,你可能会遇到一些惊喜.例如: Point point1 = new Point(20, 30); Point

  • Java程序员常犯的五个错误

    下面针对每一个错误用文字说明结合代码详解的方式展示给大家,具体内容如下: 1. Null 的过度使用 避免过度使用 null 值是一个最佳实践.例如,更好的做法是让方法返回空的 array 或者 collection 而不是 null 值,因为这样可以防止程序抛出 NullPointerException.下面代码片段会从另一个方法获得一个集合: List<String> accountIds = person.getAccountIds(); for (String accountId :

  • 手写TypeScript 时很多人常犯的几个错误

    目录 前言 1.没有使用严格模式 我们为什么要使用严格模式? 2. 使用 || 确定默认值 那它应该是什么样子的呢? 3.使用any作为类型 为什么要这么做呢? 为什么不能用any? 4. val 作为 SomeType 强制告诉编译器它无法推断的类型. 这就是类型守卫的用途. 5. any在测试用例中的表现 在编写测试时 6. 可选属性 将属性定义为有时存在,有时不存在的可选属性. 清楚地表达,模型哪些组合存在,哪些不存在. 7. 使用一个字母作为泛型参数 用一个字母给作为名称,比如常用的T作

  • 关于mysql调用新手们常犯的11个错误总结

    前言 大家可能经常收到安全部门的警告邮件,SQL注入,XSS攻击漏洞等等,偶尔还被黑客挂了小马,郁闷不?还有数据库执行太慢(根据经验基本是没有正确使用索引导致) ,下面就跟着小编来一起看看MYSQL新手们常犯的11个错误吧. 1.使用MyISAM而不是InnoDB MySQL有很多的数据库引擎,单一般也就用MyISAM和InnoDB. MyISAM是默认使用的.但是除非你是建立一个非常简单的数据库或者只是实验性的,那么到大多数时候这个选择是错误的.MyISAM不支持外键的 约束,这是保证数据完整

  • C#新手常犯的错误汇总

    本文所述为C#新手常犯的错误,但是实际上很多有经验的程序员也经常犯这些错误,对此特别整理了一下,供大家参考.具体如下: 1.遍历List的错误,比如如下代码: List<String> strList =newList<String> for(int i =0; i<strList.Count; i++) { strList.RemoveAt(i); } 这段代码看上去是删除了所有元素,实际上每次调用RemoveAt方法会导致List元素索引重排,最后导致元素没有完全删除.

  • Java开发人员最常犯的10个错误

    这个列表总结了10个Java开发人员最常犯的错误. Array转ArrayList 当需要把Array转成ArrayList的时候,开发人员经常这样做: List<String> list = Arrays.asList(arr); Arrays.asList()会返回一个ArrayList,但是要特别注意,这个ArrayList是Arrays类的静态内部类,并不是java.util.ArrayList类.java.util.Arrays.ArrayList类实现了set(), get(),c

  • 10种Java开发者编写SQL语句时常见错误

    Java开发者对于面向对象编程思维与命令行编程思维的协调程度,取决于他们如下几种能力的水平: 技巧(任何人都可以编写命令行形式的代码) 教条(有的人使用"模式 - 模式"的方式,即模式无处不在,并以名字作为标识) 情绪状况(在初期,真正面向对象形式的代码比起命令式代码会更加难懂.) 但是,当Java开发人员编写SQL语句时,一切都变得不同了.SQL是一种说明式语言,与面向对象思想和命令式思想无关.在SQL语言中,查询非常容易表达.但它也不是那么容易以最佳或最正确地方式编写出来.开发人员

  • python 使用三引号时容易犯的小错误

    请看如下代码,执行后,思考生成的两个二维码为什么不一样? # -*- coding:utf-8 -*- from tkinter import * from tkinter import ttk from PIL import ImageTk import qrcode class QRcodeImage(object): '''生成二维码图片类''' def __init__(self, content, fcolor=None, bcolor=None, size=None): ''' 参数

随机推荐