xufei/blog

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 工程化中的坑王。

hax commented

嗯,不知道这个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可能更好.

@jinwyp 如果只在自己的一个项目中,怎么搞都是可以的。你考虑把项目中的一个或多个部分拿出去在几个项目之间共享,尤其是这些公共部分要做懒加载的时候,就比较难受了。

@xufei
我认为,楼主误解了 module。它不是 namespace,只是作为一个复用单位来处理强内聚的不同构件(controller、directive、factor、service 等为“构件”)。所以,module 已经达到了预期目的,只是不能达到楼主臆想的那种预期。

其次,动态加载机制的不是推荐方式,这可以说是 requirejs 的问题。其实 requirejs 一直都有这个问题,对于一些有自身的生命周期管理的框架,难以切入。在WEB JS框架设计上,生命周期管理应该形成一个闭环,有一些与 browser 事件绑定的生命周期节点,使用 requirejs 完全无法处理闭环。所以延迟加载不是 angular 的坑,而是 requirejs 的缺陷。

另外一篇文章在哪儿呢?@xufei

所以模块复用的时候,服务不要用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

xufei commented

@hjzheng @chinfeng 我这个不是误解,是说会有这么一种预期,比如一个新上手的人,他有很大概率会这么预期。。。

xufei commented

所以,前年我基于angular搞一个平台规划的时候,是这样的:

#7

其中有一段:

所以我们期望的是,在每个编写的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