Angular沉思录(三)Angular中的模块机制
xufei opened this issue · 19 comments
Angular中的模块机制
module
在AngularJS中,有module的概念,但是它这个module,跟我们通常在AMD里面看到的module是完全不同的两种东西,大致可以相当于是一个namespace,或者package,表示的是一堆功能单元的集合。
一个比较正式的Angular应用,需要声明一个module,供初始化之用。比如说:
angular.module("test", [])
.controller("TestCtrl", ["$scope", function($scope) {
$scope.a = 1;
}]);
随后,可以在HTML中指定这个module:
<div ng-app="test" ng-controller="TestCtrl">
{{a}}
</div>
这样,就是以这个div为基准容器,实例化了刚才定义的module。
或者,也可以等价地这样,在这里,我们很清楚地看到,module的意义是用于标识在一个页面中可能包含的多个Angular应用。
angular.element(document).ready(function() {
angular.bootstrap(document.getElementById("app1"), ["test"]);
angular.bootstrap(document.getElementById("app2"), ["test"]);
});
<div id="app1" ng-controller="TestCtrl">
{{a}}
</div>
<div id="app2" ng-controller="TestCtrl">
{{a}}
</div>
这样可以在同一个页面中创建同一module的不同实例。两个应用互不干涉,在各自的容器中运行。
module的依赖项
除此之外,我们可以看到,在module声明的时候,后面带一个数组,这个数组里面可以指定它所依赖的module。比如说:
angular.module("moduleB", [])
.service("GreetService", function() {
return {
greet: function() {
return "Hello, world";
}
};
});
angular.module("moduleA", ["moduleB"])
.controller("TestCtrl", ["$scope", "GreetService", function($scope, GreetService) {
$scope.words = "";
$scope.greet = function() {
$scope.words = GreetService.greet();
};
}]);
然后对应的HTML是:
<div ng-app="moduleA">
<div ng-controller="TestCtrl">
<span ng-bind="words"></span>
<button ng-click="greet()">Greet</button>
</div>
</div>
好了,注意到这个例子里面,创建了两个module,在页面上只直接初始化了moduleA,但是从moduleA的依赖关系中,引用到了moduleB,所以,moduleA下面的TestCtrl,可以像引用同一个module下其他service那样,引用moduleB中定义的service。
到这里,我们是不是就可以把module当作一种namespace那样的组织方式呢,很可惜,它远远没有想的那么好。
这种module真的有用吗?
看下面这个例子:
angular.module("moduleA", [])
.factory("A", function() {
return "a";
})
.factory("B", function() {
return "b";
});
angular.module("moduleB", [])
.factory("A", function() {
return "A";
})
.factory("B", function() {
return "B";
});
angular.module("moduleC", ["moduleA", "moduleB"])
.factory("C", ["A", "B", function(A, B) {
return A + B;
}])
.controller("TestCtrl", ["$scope", "C", function($scope, C) {
$scope.c = C;
}]);
angular.module("moduleD", ["moduleB", "moduleA"])
.factory("C", ["A", "B", function(A, B) {
return A + B;
}])
.controller("TestCtrl", ["$scope", "C", function($scope, C) {
$scope.c = C;
}]);
angular.module("moduleE", ["moduleA"])
.factory("A", function() {
return "AAAAA";
})
.factory("C", ["A", "B", function(A, B) {
return A + B;
}])
.controller("TestCtrl", ["$scope", "C", function($scope, C) {
$scope.c = C;
}]);
<div id="app1" ng-controller="TestCtrl">
<span ng-bind="c"></span>
</div>
<div id="app2" ng-controller="TestCtrl">
<span ng-bind="c"></span>
</div>
<div id="app3" ng-controller="TestCtrl">
<span ng-bind="c"></span>
</div>
angular.element(document).ready(function() {
angular.bootstrap(document.getElementById("app1"), ["moduleC"]);
angular.bootstrap(document.getElementById("app2"), ["moduleD"]);
angular.bootstrap(document.getElementById("app3"), ["moduleE"]);
});
我们在moduleA和moduleB中,分别定义了两个A跟B,然后,在moduleC和moduleD的时候中,分别依赖这两个module,但是依赖的顺序不同,其他所有代码完全一致,再看看结果,会发现两边的结果居然是不一致的。
再看看moduleE,它自己里面有一个A,然后结果跟前两个例子也是不同的。
照理说,我们对module会有一种预期,也就是把它当作命名空间来使用,但实际上它并未起到这种作用,只是一个简单的复制,把依赖的module中定义的东西全部复制到自己里面了,后面进来的会覆盖前面的,比如:
- moduleC里面,来自moduleA的两个变量被来自moduleB的覆盖了
- moduleD里面,来自moduleB的两个变量被来自moduleA的覆盖了
- moduleE里面,来自moduleA的A被moduleE自己里面的A覆盖了,因为它的A是后加进来的
整个覆盖过程没有任何提示。
我们可以把module设计的初衷理解为:供不同的开发团队,或者不同的业务模块做归类约束用,但实际上完全没有起到这种作用。结果,不得不在下级组织单元的命名上继续做文章,不然在多项目集成的时候,就要面临冲突的风险。
更多的坑
不仅如此,这种module机制还为大型应用造成了不必要的麻烦。比如说,module不支持运行时添加依赖,看下面的例子:
angular.module("some.components", [])
//这里定义了一些组件
;
假设上面是一个组件库,集中存放于components.js中,我们要在自己的应用中使用,必须:
angular.module("our.app", ["some.components"]);
现在假设这个components.js较大,我们不打算在首页引入,想在某个时候动态加载,就会出现这样的尴尬局面:
- 主应用our.app启动的时候,必须声明所有依赖项
- 但是它所依赖的module "some.components"的声明还在另外一个未加载的文件components.js中
关键问题就在于它不存在一个在our.app启动之后向其中添加some.components依赖的方式。我们预期的代码方式是类似这样:
angular.module("our.app", []);
require("components.js", function() {
// angular.module("our.app").addDependency("some.components");
// ready to use
});
也就是这段代码中注释掉的那句。但从现在看来,它基本没法做这个,因为他用的是复制的方式,而且对同名的业务单元不做提示,也就是可能出现覆盖了已经在使用的模块,导致同一个应用中的同名业务单元出现行为不一致的情况,对排错很不利。
在一些angular最佳实践中,建议各业务模块使用module来组织业务单元,基于以上原因,我个人是不认同的,我推荐在下一级的controller,service,factory等东西上,使用标准AMD的那种方式定义名称,而彻底放弃module的声明,比如所有业务代码都适用同一个module。详细的介绍,我会在另外一篇文章中给出。
此外,考虑到在前端体系中,JavaScript是需要加载到浏览器才能使用的,module的机制自身也至少应当包括异步加载机制,很可惜,没有。没有模块加载机制,意味着什么呢?意味着做大型应用有麻烦。这个可以用一些变通的方式去处理,在这里先不提了。
可以看到,Angular中的module并未起到预期作用,相反,还造成了一些麻烦。因此,我认为这是Angular当前版本中唯一一块弊大于利的东西,在2.0中,这部分已经做了重新规划,会把这些问题解决,也加入动态加载的考虑。
赞,这个 module 机制是 Angular 工程化中的坑王。
嗯,不知道这个module部分当初是谁挖的坑,有那么多珠玉在前,还做得那么烂,应该拖出来打一顿,呵呵。
在 angular 早期的业务场景中,也不算是坑。用的人越来越多,变成了巨坑……
不能同意更多,开始的时候看到module,哎呦不错这个好!后来发现和RequireJS等等加载器配合都各种不顺畅,只好把所有的JS都在加载时候一块load进来完事儿
我们的做法是在定义module时,加入其他module的依赖;然后其他的module都只是定义一个module,仅此而已。如同上边components的例子,是不是可以这样:
/** main.js **/
// require components.js
angular.module("our.app", ['components']);
/** components.js **/
return angular.module('components', []);
/** componentA.js **/
require(["components.js"], function(componentsModule) {
// 这样 往componentsModule上去“挂载”东西
componentsModule.xxx = xxx
});
然后在需要的地方,利用requirejs
等加载器去加载所需要的componentA.js
这个文件就好了。这样也不必担心在一个页面资源浪费的情况了。当然,覆盖的情况还是会有的,看规划或者约定吧。
纯属个人浅见。
@dolymood 我明白你的意思,就是专门弄一些空壳子,这些js文件中,每个只放module的声明对吧,这个可以解决问题,但主module必须把所有的壳子module文件全部加载进来,并且在依赖中包含。真正的业务实现的js中,不能声明module,只能使用。这么一来,这些壳子就纯属浪费了,所以我觉得还不如不要,所有的都挂主module的名字下
我想知道.... angular干嘛搞这么个module. 我实在想不通, 除了可以使用别名注入之外, 毫无用处啊, 怪不得2.0没了 可喜可贺
"在一些angular最佳实践中,建议各业务模块使用module来组织业务单元,基于以上原因,我个人是不认同的,我推荐在下一级的controller,service,factory等东西上,使用标准AMD的那种方式定义名称,而彻底放弃module的声明,比如所有业务代码都适用同一个module。"
想想还真是, 但这样出错更难找出模块的问题, 我现在是有个主命名空间, 例如 模块appmain, 其他的模块为appmain.model, appmain.directives , appmain.components 这样.
我觉得angular的模块坑不吭看你怎么理解, angular 模块我感觉根本就不是amd,cmd这种规范, angular的模块其实和javascript 的代码结构, 文件加载, 依赖 关系都不大.
angular的模块更像是功能的命名空间, 就是一个app 或一个网站是一个模块 这种概念, 有点像组件的命名空间, 而不是javascript代码的模块管理. module "模块"可以翻译为"项目" 更好理解, module 其实改成project可能更好.
@xufei
我认为,楼主误解了 module。它不是 namespace,只是作为一个复用单位来处理强内聚的不同构件(controller、directive、factor、service 等为“构件”)。所以,module 已经达到了预期目的,只是不能达到楼主臆想的那种预期。
其次,动态加载机制的不是推荐方式,这可以说是 requirejs 的问题。其实 requirejs 一直都有这个问题,对于一些有自身的生命周期管理的框架,难以切入。在WEB JS框架设计上,生命周期管理应该形成一个闭环,有一些与 browser 事件绑定的生命周期节点,使用 requirejs 完全无法处理闭环。所以延迟加载不是 angular 的坑,而是 requirejs 的缺陷。
所以模块复用的时候,服务不要用factory。应该使用provider可以合理控制作用域。但小型应用中,真不建议自己搞很多模块,一个模块完事儿就好。
@xufei 在app启动后动态去添加依赖还是有方法的,方法参照这个库ocLazyLoad
前两天基于ui-router跟ocLazyLoad实现了一套无侵入式的angular按需加载方案,代码在这里ui-router-require-polyfill,blog在这里基于ui-router的无侵入式angular按需加载方案
@ chinfeng 非常同意你的观点。
楼主是误解了angular1.x的module,翻了一下官方的doc https://docs.angularjs.org/guide/module。
第二,关于相同名字service在不同模块中会覆盖,这个是与其依赖注入(DI)设计有关的。
当然楼主的担心是正确的,随着项目变的庞大,它就不能满足大家的期望了,庆幸的是angular团队及时果断的推出2.0
所以,前年我基于angular搞一个平台规划的时候,是这样的:
其中有一段:
所以我们期望的是,在每个编写的JavaScript模块中只存放具体实现,而把依赖关系放在我们的平台上管理,这样,即使当前模块作了改名之类的重构处理,处于外部项目中依赖它的那些代码也不必修改,下一次版本发布的生成过程会自动把这些事情干掉。
意思就是,模块只存放内部的controller那些实现,依赖关系外置到这个管理平台(预期是类似npm)中,然后在构建的时候,给它生成这些module的配置。这样,如果有一天我们需要把目录整体调整,也不至于到处要修改这个模块名。
如果不这样,我们就面临这样:
angular.module("aa.bb.cc").controller("aa.bb.cc.ControllerD", [...])
这样的写法,然后模块如果需要做业务上的调整,比如下沉一级目录,这些controller上面的名称都得跟着改,否则就会出现物理路径和逻辑路径不一致,也容易出现冲突。
angular 1.x其实在合理规划下,是足够支撑大型项目的,但这个module机制反而制约了它在大型工程上的使用体验,所以沦为中小型方案。其他部分并没有这么严重的缺点。
@myst729 你看看
@xufei 又读了一遍,理解,是一种预期。其实你遇到的问题,相信大家也都遇到过,但是没有像你一样总结思考出来,所以感谢啊!关于楼主所提到的问题,有个ng-conf的视频,中间有一段提到说是Angular1.x的DI module的缺点: https://www.youtube.com/watch?v=_OGGsf1ZXMs&index=1&list=PLw5h0DiJ-9PB-vLe3vaNFLG-cTw0Wo7fw
Isn’t the current DI in ng 1.2 good enough? It is, but I believe we can make it better.
It has issues:
• creating DI modules (hard to integrate with module loaders; RequireJS, TypeScript)
• complex API (provider, service, factory, value, filter, …)
• confusing config/run phase
• string ids make it hard to minify
• also conflicts of ids (because there is no namespacing)
• really hard to lazy load code
• all instances are singletons
I claim that the new DI system that I’m about to show you, solves these issues.
angular最佳实践那一部分,使用ADM方式定义。 有具体的例子吗?
“module不支持运行时添加依赖”,踩过坑的路过,顺便奉上解决办法http://www.cnblogs.com/wangmeijian/p/5020788.html