最近的项目使用了AngularJS作为主要前端框架,在使用模块时遇到一些有意思的问题,本文围绕模块的定义与加载的话题,小结一下我的体会。
首先,定义一个Angular模块(module)很简单,直接使用angular模块的module方法即可,如:
var myApp = angular.module('myApp', []);
值得注意的是,module函数的第二个参数定义了该模块所依赖的模块。在首次定义一个模块时,必须同时指定模块名和依赖,尽管该模块可能没有依赖(使用空数组)。如果不指定依赖,则Angular会试图定位已经定义好的名称为myApp的模块,这通常会导致模块未定义错误。如果依赖不为空,则要保证其依赖的模块已经加载。这里面就有个加载顺序的问题。
最简单的情况下,我把使用模块所在的JS文件全部按顺序列在HTML的script元素中,只要保证模块之间的依赖关系正确即可,如:
<!doctype html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="utf-8">
<title>My HTML File</title>
<link rel="stylesheet" href="css/app.css">
<script src="/scripts/myApp.js"></script>
<script src="/scripts/app.js"></script>
</head>
<body>
<div>
Hello Angular !
</div>
</body>
</html>
如果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的自动初始化发生在两个时机:
- 响应DOMContentLoaded事件(页面文档完全加载并解析完毕后会触发该事件,不会等待图片、样式文件)
- document.readyState的值为'complete'。(兼容IE8,页面完全加载,相当于load事件触发)
这时,Angular会调用angularInit方法进行初始化,首先查找ngApp directive,该directive指定了应用程序的根结点,通常位于初始页面的<html>标记上。当然,ngApp如果位于某个子结点(如div元素),则可以将Angular应用程序限制在DOM树的一部分上。如果Angular找到了ngApp,则会调用bootstrap方法开始启动过程,主要工作如下:
- 加载ngApp指定的模块
- 创建应用程序的依赖注入对象(injector)。此时$compile,$rootScope会被注入,用于后续的compile过程。有关依赖注入过程有后文。
- 以ngApp为根结点,compile整个DOM树。有关compile过程,在后续文章中详谈。
<script>
angular.module('myApp', [])
.controller('MyController', ['$scope', function ($scope) {
}]);
angular.element(document).ready(function() {
angular.bootstrap(document, ['myApp']);
});
</script>
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']]);
});
});
});
有几点需要注意:
- 使用手动启动时,不要再指定ngApp directive。
- 在使用延迟启动时需要指定ngApp或者手动调用bootstrap方法,否则会报错:resumeBootstrap不是一个函数。原因是指定ngApp会内部调用bootstrap方法,而resumeBootstrap方法定义于bootstrap方法内部,如果没有指定ngApp,又没有手动调用bootstrap,则resumeBootstrap未定义。
- 如果在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官方文档及源代码)