AngularJS开发者常犯的10个错误

Mark Meyer是一个有超过一年angular.js实际开发经验的full stack软件工程师。 Mark拥有多种语言的开发经验,从基于C的服务器应用,基于Rails的web应用到使用Swift开发的IOS应用。

简介

AngularJS是目前最流行的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使之非常善于构建小型app原型,但它也能够用于开发功能全面的客户端应用。便于开发,特性广泛以及出众的性能使其被广泛使用,然而,大量常见陷阱也随之而来。以下这份列表摘取了一些常见的AngularJS的错误用法,尤其是在app规模扩张时。

1. 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文件放在一起可能存在争议,但节省下来的时间更加宝贵。

2. 模块(或者没有模块)

开始时将所有东西都放在主模块下是很常见的。对于小型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']);
当开发一个大型应用程序时,不可能将所有东西都包含在一个页面上,按特性划分模块使得在应用间复用模块变得更加容易。

3. 依赖注入

依赖注入是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(而且应该使用!),那这就是必要的了。

4. Controller膨胀

Controller是AngularJS最基本的部分。一不小心就会将过多的逻辑放到其中,尤其是刚开始的时候。控制器永远都不应该去操作DOM或是持有DOM选择器,那是directive和ng-model的用武之地。同样的,业务逻辑应该存在于service中,而非controller。
数据也应该存储在service中,除非它们已经被绑定在$scope上了。服务本身是单例的,存在于应用程序的整个生命周期中,然而controller在应用程序的各状态间是瞬态的。如果数据被保存在控制器中,当它再次被实例化时就需要重新从某处获取数据。即使将数据存储于localStorage中,获取的速度也要比从Javascript变量中要慢一个数量级。
AngularJS在遵循单一职责原则(SRP)时运行良好。如果controller是视图和模型间的协调者,那么它所包含的逻辑就应该尽量少。这也会让测试更加简单。

5. 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函数,而factory又调用了provider函数。事实上,AngularJS也提供了一些额外的provider封装,如value,constant和decorator。但这些并没有导致类似程度的困惑,它们的文档在应用场景上说得相当清晰。
既然service仅仅是调用了factory函数,那二者有什么区别呢?线索在$injector.instantiate;在这个函数中,$injector在service的构造函数中创建了一个新的实例。
以下是一个例子,展示了一个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方法,返回“hello world”。service的构造函数在声明时被实例化了一次,而factory对象在每一次被注入时传递一次,但是仍然只有一个factory实例。。

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

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

这里是一个使用了service和两个factory的controller的例子。helloFactory返回了一个函数,当被new时会设置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中更加直观。

6. 没有使用Batarang

Batarang是一个出色的Chrome插件,用来开发和调试AngularJS应用。
Batarang提供了浏览模型的能力,从而能够观察到AngularJS内部是如何确定绑定到作用域上的模型的。这在观察directive和isolate scopes的绑定值时非常有用。
Batarang也提供了一个依赖图。如果用到一个未经测试的代码库时,这个依赖图能够用于决定哪些服务应该被重点关注。
最后,Batarang提供了性能分析。AngularJs性能良好,然而对于有越来越多的自定义directive和复杂逻辑的应用来说,有时候显得不够流畅。使用Batarang性能工具,能够很容易地看到在一个digest周期中哪个函数的运行时间最长。这个工具也能展示一棵完整的watch树,这在有很多watcher时很有用。

7. 过多的watcher

在上一点中提到,AngularJS性能良好。由于要在一个digest周期中完成脏数据检查,一旦watcher的数量增长到大约2000时,这个周期就会产生显著的性能问题。(2000这个数字并不一定会造成性能大幅下降,但这是一个不错的经验数值。在AngularJS 1.3 release版本中,已经有一些允许严格控制digest周期的改动了,Aaron Gray有一篇在这个问题上的很好的文章。)
以下这个“立即执行的函数表达式(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数量增长。

8. 确定$scope的范围

Javascript基于原型的继承与面向对象中基于类的继承有着微妙的区别。这通常不是什么问题,但在使用$scope时就会表现出来。在AngularJS中,每个$scope都会继承父$scope,最高层是$rootScope。($scope用在directive中时有些不同,isolate scopes只继承显式声明的属性。)
由于原型继承的特点,在父类和子类间共享数据不太重要。但如果不够小心,很容易覆盖掉父$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模版和$scope会同loginCtrl的一起被更新。
这个例子有些勉强,但是当你使用directive去创建子$scope,如ngRepeat时,这个问题很容易就会产生。

9. 手工测试

由于TDD可能不是每个开发人员都喜欢的开发方式,因此当开发人员检查代码是否工作或是否影响了其它东西时,他们会做手工测试。
不测试AngularJS应用是没有道理的。AngularJS的设计使之完全可测。依赖注入和ngMock模块就是明证。AngularJS核心团队已经开发了众多能够使测试更上一层楼的工具。

9.1 Protractor

单元测试是测试套的基础,但考虑到app的日益复杂,集成测试被迫更加现实。幸运的是,AngularJS的核心团队已经提供了必要的工具。
我们已经建立了Protractor,一个用于模拟用户交互的端到端的测试器,它能够帮助你验证你的AngularJS程序的健康状况。
Protractor使用Jasmine测试框架定义测试,Protractor针对不同的页面交互行为有一个非常健壮的API。
还有一些其他的端到端测试工具,但是Protractor的优势在于能够理解如何与AngularJS代码协同工作,尤其是在$digest周期中。

9.2 Karma

一旦我们用Protractor完成了集成测试的编写工作,接下去就是执行测试了。等待测试执行,尤其是集成测试,对每个开发人员来说都是痛苦的事情。AngularJS的核心团队感同身受,于是开发了Karma。
Karma是一个Javascript测试执行工具,它有助于工作闭环。Karma之所以能够做到这点,是因为它在指定文件被改变时就运行测试。Karma同时也能够在多个浏览器上并行执行测试。不同的设备也可以指向Karma服务器,以更好地覆盖真实的应用场景。

10. 使用jQuery

jQuery是一个神奇的库,它标准化了跨平台开发,几乎已经成为现代Web开发的必需品。不过尽管JQuery如此多的优秀特性,它的理念和AngularJS并不一致。
AngularJS是一个用来构建应用的框架,而JQuery则是一个简化“HTML文档遍历和操作、事件处理、动画和Ajax”的库。这是两者最基本的区别,AngularJS致力于应用的架构,与HTML页面无关。
为了更好的理解如何建立一个AngularJS程序,请停止使用jQuery。jQuery使开发人员以现存的HTML标准思考问题,但正如文档里所说的,“AngularJS能够让你为你的应用扩展HTML的词汇表”。
DOM操作应该只在directive中完成,但这并不意味着他们只能用jQuery封装。在你使用JQuery之前,应该先想一下这个功能是不是AngularJS已经提供了。当directives结合时能够创建强大的工具,这工作得相当好。
这一天可能会到来,一个非常棒的JQuery成为必需,但在一开始就引入它,是一个常见的错误。

结论

AngularJS是一个伟大的框架,在社区中不断演进。符合习惯的AngularJS仍然是一个不断发展的概念,但希望通过遵循以上谈到的这些约定,能够避免遇到开发大规模AngularJS应用时的一些大的陷阱。



原文链接:https://www.airpair.com/angularjs/posts/top-10-mistakes-angularjs-developers-make

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。