从 AngularJS 模块定义说起

最近的项目使用了AngularJS作为主要前端框架,在使用模块时遇到一些有意思的问题,本文围绕模块的定义与加载的话题,小结一下我的体会。

首先,定义一个Angular模块(module)很简单,直接使用angular模块的module方法即可,如:

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

值得注意的是,module函数的第二个参数定义了该模块所依赖的模块。在首次定义一个模块时,必须同时指定模块名和依赖,尽管该模块可能没有依赖(使用空数组)。如果不指定依赖,则Angular会试图定位已经定义好的名称为myApp的模块,这通常会导致模块未定义错误。如果依赖不为空,则要保证其依赖的模块已经加载。这里面就有个加载顺序的问题。

最简单的情况下,我把使用模块所在的JS文件全部按顺序列在HTML的script元素中,只要保证模块之间的依赖关系正确即可,如:




  
  My HTML File
    
  
  


Hello Angular !

如果app.js中的模块依赖模块myApp,则需先将定义它的myApp.js加载进来。在项目实践中,这种方法显然是不实际的,你不能在一开始把所有的JS文件全部加载进来。通常会使用一些延迟/异步加载机制,如使用RequireJS。

使用RequireJS定义的Angular模块可以是这样:

define(['angular'], function(angular) {
    angular.module('myApp', [])
      .controller('MyController', ['$scope', function ($scope) {
        //define scope data
      }]);
});
其中define函数加载了angular.js,并取得angular模块,然后定义了myApp模块。如果myApp模块(或者MyController)依赖于其他模块(或组件),则需要告诉RequireJS在需要的时候加载:

define(['angular', 
	'./scripts/anotherApp', 
	'./scripts/service/utility' 
	], function(angular) {
	 	angular.module('myApp', ['newApp']) 
	 		.controller('MyController', ['$scope', 'utils', function ($scope, utils) { 
	 		//define scope data 
	 	}]); 
});

这里,myApp依赖定义于anotherApp.js的newApp模块,并且需要定义于utility.js的工厂服务utils。这里存在的一个陷阱是,依赖的两个JS文件不一定会按照代码中声明的顺序加载,即utility.js可能先于anotherApp.js加载。因此在使用requireJS加载依赖时,要注意这些依赖本身之间的相互关系,不能指望requires按某种顺序加载这些依赖。否则,会导致一些概率性出现的问题,不易调试。

另外一点,如果在首次定义模块A时使用了空数组作为依赖,然后在某次使用A模块的过程中,误将依赖数组又传递一次,那么A模块原来的定义就会被覆盖,并且原来A模块定义的controller,service等组件也将不存在,这样也会导致难以调试的问题。因此在定义和使用Angular模块时不能大意。

-------------------------------------------------------------------------------------------------------------

2016/03/19 Update

谈到模块加载这个话题,就不能不提及Angular的启动(bootstrap)机制。简单来说,Angular的启动分为手动自动两种。在前面的例子中,采用的就是自动的方式:通过内置的directive ngApp 来指定启动时加载的模块。

根据官方文档(这里),Angular的自动初始化发生在两个时机:

  1. 响应DOMContentLoaded事件(页面文档完全加载并解析完毕后会触发该事件,不会等待图片、样式文件)
  2. document.readyState的值为'complete'。(兼容IE8,页面完全加载,相当于load事件触发)

这时,Angular会调用angularInit方法进行初始化,首先查找ngApp directive,该directive指定了应用程序的根结点,通常位于初始页面的标记上。当然,ngApp如果位于某个子结点(如div元素),则可以将Angular应用程序限制在DOM树的一部分上。如果Angular找到了ngApp,则会调用bootstrap方法开始启动过程,主要工作如下:

  1. 加载ngApp指定的模块
  2. 创建应用程序的依赖注入对象(injector)。此时$compile,$rootScope会被注入,用于后续的compile过程。有关依赖注入过程有后文。
  3. 以ngApp为根结点,compile整个DOM树。有关compile过程,在后续文章中详谈。

这个过程如下图所示。(图片来源:https://docs.angularjs.org/guide/bootstrap)。
从 AngularJS 模块定义说起_第1张图片

有些情况下,开发者需要在初始化阶段做些额外的配置或控制,比如include一些模块或者需要在compile过程之前完成一些工作。这就需要手动启动。手动启动通过angular.bootstrap方法。一个例子如下:


该方法的第一个参数指定了Angular应用所在的根DOM元素;第二个参数定义了需要依赖加载的模块(相当于ngApp),该模块必须事先定义完成(包括其controller,directive等组件),bootstrap方法不会定义该模块。(该方法还有第三个参数,参见这里)。一个Anuglar应用一旦启动,就不能再添加controller, service等组件。
有时,开发者不仅需要在启动阶段添加功能,还需要等待一些依赖项而延迟启动过程。这时可以使用angular.resumeBootstrap方法。如果window.name包含前缀NG_DEFER_BOOTSTRAP!,则当angular.bootstrap方法被调用时,启动过程会暂停,直到调用angular.resumeBootstrap方法。该方法会向启动需要的模块列表里加入额外的模块。例如:

window.name = "NG_DEFER_BOOTSTRAP!";
requirejs.config({
  baseUrl: '.',
  paths: {
  	app: 'app/scripts/app',
    jquery: 'common/lib/jquery/dist/jquery',
    angular: 'common/lib/angular/angular'
  },
  shim: {
    angular : { 
    	exports : 'angular'
    }
  }
});

require(['angular', 'jquery'], function(){
	require(['app'], function(app) {
	  angular.element().ready(function() {
	        angular.resumeBootstrap([app['name']]);
	   });
	});
});

有几点需要注意:

  1. 使用手动启动时,不要再指定ngApp directive。
  2. 在使用延迟启动时需要指定ngApp或者手动调用bootstrap方法,否则会报错:resumeBootstrap不是一个函数。原因是指定ngApp会内部调用bootstrap方法,而resumeBootstrap方法定义于bootstrap方法内部,如果没有指定ngApp,又没有手动调用bootstrap,则resumeBootstrap未定义。
  3. 如果在HTML指定了多个Angular实例(如多次加载angular.js),Angular只会启动第一个加载成功的实例启动。如果不延迟启动,则angular在遇到ng-app directive时开始初始化,并加载相应模块。如果需要加载额外的模块而需要延迟启动,则需定义window.name为NG_DEFER_BOOTSTRAP!,然后在所有模块加载完毕后,调用resumeBootstrap方法,继续启动,并加载指定模块。angular在自动加载时会调用bootstrap方法,如果没有指定ng-app,则需手动调用bootstrap方法。由于resumeBootstrap方法定义于bootstrap方法内部,因此如果bootstrap没有调用,就会报resumeBootstrap is not a function的错误。

(本文参考了AngularJS官方文档及源代码)


你可能感兴趣的:(Angular)