Swift51.com
麦子学院 头像
麦子学院  2017-05-15 12:42

AngularJS开发的一些注意事项

回复:0  查看:2353  
AngularJS 是目前最为活跃的 Javascript 框架之一, AngularJS 的目标之一是简化开发过程,这使得 AngularJS 非常善于构建小型 app 原型,但 AngularJS 对于全功能的客户端应用程序同样强大,它结合了开发简便,特性广泛和出众的性能,使其被广泛使用。然而,大量使用也会产生诸多误区。本文和大家分享的就是AngularJS 开发最常犯的错误,一起来看看吧,希望对大家 学习AngularJS有所帮助。
  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']);
  当我们开发一个大型应用程序时,可能并不是所有东西都包含在一个页面上。将同一类特性置于一个模块内,能使跨app 间重用模块变得更容易。
  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.  控制器膨胀
  控制器是AngularJS 的肉和土豆,一不小心就会将过多的逻辑加入其中,尤其是刚开始的时候。控制器永远都不应该去操作 DOM ,或是持有 DOM 选择器,那是我们需要使用指令和 ng-model 的地方。同样的,业务逻辑应该存在于服务中,而非控制器。
  数据也应该存储在服务中,除非它们已经被绑定在$scope 上了。服务本身是单例的,在应用程序的整个生命周期都存在,然而控制器在应用程序的各状态间是瞬态的。如果数据被保存在控制器中,当它被再次实例化时就需要重新从某处获取数据。即使将数据存储于 localStorage 中,检索的速度也要比 Javascript 变量慢一个数量级。
  AngularJS 在遵循单一职责原则( SRP )时运行良好,如果控制器是视图和模型间的协调者,那么它所包含的逻辑就应该尽量少,这同样会给测试带来便利。
  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 函数,而后者又调用了 provider 函数。事实上, AngularJS 也为一些值、常量和装饰提供额外的 provider 封装,而这些并没有导致类似的困惑,它们的文档都非常清晰。
  由于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 被注入到控制器中,它们都有一个 hello 方法,返回 ”hello world” service 的构造函数在声明时被实例化了一次,同时 factory 对象在每一次被注入时传递,但是仍然只有一个 factory 实例。所有的 providers 都是单例。
  既然能做相同的事,为什么需要两种不同的风格呢?相对于service factory 提供了更多的灵活性,因为它可以返回函数,这些函数之后可以被新建出来。这迎合了面向对象编程中工厂模式的概念,工厂可以是一个能够创建其他对象的对象。
  app.factory('helloFactory', function() {
  return function(name) {
  this.name = name;
  this.hello = function() {
  return "Hello " + this.name;
  };
  };
  });
  这里是一个控制器示例,使用了service 和两个 factory helloFactory 返回了一个函数,当新建对象时会设置 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 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 上的回答:
  (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 与传统指令有些不同,它们有一定的作用范围 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 模版和 $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 应用所遇到的那些问题。


来源:网络