Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular沉思录(三)Angular中的模块机制 #17

Open
xufei opened this issue Jan 22, 2015 · 20 comments
Open

Angular沉思录(三)Angular中的模块机制 #17

xufei opened this issue Jan 22, 2015 · 20 comments

Comments

@xufei
Copy link
Owner

xufei commented Jan 22, 2015

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中,这部分已经做了重新规划,会把这些问题解决,也加入动态加载的考虑。

@xufei xufei changed the title Angular沉思录(三)——Angular中的模块机制 Angular沉思录(三)Angular中的模块机制 Jan 22, 2015
@afc163
Copy link

afc163 commented Jan 22, 2015

赞,这个 module 机制是 Angular 工程化中的坑王。

@hax
Copy link

hax commented Jan 22, 2015

嗯,不知道这个module部分当初是谁挖的坑,有那么多珠玉在前,还做得那么烂,应该拖出来打一顿,呵呵。

@lifesinger
Copy link

在 angular 早期的业务场景中,也不算是坑。用的人越来越多,变成了巨坑……

@wizcabbit
Copy link

不能同意更多,开始的时候看到module,哎呦不错这个好!后来发现和RequireJS等等加载器配合都各种不顺畅,只好把所有的JS都在加载时候一块load进来完事儿

@dolymood
Copy link

我们的做法是在定义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这个文件就好了。这样也不必担心在一个页面资源浪费的情况了。当然,覆盖的情况还是会有的,看规划或者约定吧。
纯属个人浅见。

@xufei
Copy link
Owner Author

xufei commented Jan 27, 2015

@dolymood 我明白你的意思,就是专门弄一些空壳子,这些js文件中,每个只放module的声明对吧,这个可以解决问题,但主module必须把所有的壳子module文件全部加载进来,并且在依赖中包含。真正的业务实现的js中,不能声明module,只能使用。这么一来,这些壳子就纯属浪费了,所以我觉得还不如不要,所有的都挂主module的名字下

@leeluolee
Copy link

我想知道.... angular干嘛搞这么个module. 我实在想不通, 除了可以使用别名注入之外, 毫无用处啊, 怪不得2.0没了 可喜可贺

@jinwyp
Copy link

jinwyp commented Feb 4, 2015

"在一些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
Copy link
Owner Author

xufei commented Feb 4, 2015

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

@chinfeng
Copy link

chinfeng commented Mar 8, 2015

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

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

@kuitos
Copy link

kuitos commented Apr 21, 2015

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

@cuitianze
Copy link

所以模块复用的时候,服务不要用factory。应该使用provider可以合理控制作用域。但小型应用中,真不建议自己搞很多模块,一个模块完事儿就好。

@kuitos
Copy link

kuitos commented Dec 18, 2015

@xufei 在app启动后动态去添加依赖还是有方法的,方法参照这个库ocLazyLoad
前两天基于ui-router跟ocLazyLoad实现了一套无侵入式的angular按需加载方案,代码在这里ui-router-require-polyfill,blog在这里基于ui-router的无侵入式angular按需加载方案

@hjzheng
Copy link

hjzheng commented Dec 21, 2015

@ chinfeng 非常同意你的观点。
楼主是误解了angular1.x的module,翻了一下官方的doc https://docs.angularjs.org/guide/module。
第二,关于相同名字service在不同模块中会覆盖,这个是与其依赖注入(DI)设计有关的。

当然楼主的担心是正确的,随着项目变的庞大,它就不能满足大家的期望了,庆幸的是angular团队及时果断的推出2.0

@xufei
Copy link
Owner Author

xufei commented Dec 21, 2015

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

@xufei
Copy link
Owner Author

xufei commented Dec 21, 2015

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

#7

其中有一段:

所以我们期望的是,在每个编写的JavaScript模块中只存放具体实现,而把依赖关系放在我们的平台上管理,这样,即使当前模块作了改名之类的重构处理,处于外部项目中依赖它的那些代码也不必修改,下一次版本发布的生成过程会自动把这些事情干掉。

意思就是,模块只存放内部的controller那些实现,依赖关系外置到这个管理平台(预期是类似npm)中,然后在构建的时候,给它生成这些module的配置。这样,如果有一天我们需要把目录整体调整,也不至于到处要修改这个模块名。

如果不这样,我们就面临这样:

angular.module("aa.bb.cc").controller("aa.bb.cc.ControllerD", [...])

这样的写法,然后模块如果需要做业务上的调整,比如下沉一级目录,这些controller上面的名称都得跟着改,否则就会出现物理路径和逻辑路径不一致,也容易出现冲突。

angular 1.x其实在合理规划下,是足够支撑大型项目的,但这个module机制反而制约了它在大型工程上的使用体验,所以沦为中小型方案。其他部分并没有这么严重的缺点。

@myst729 你看看

@hjzheng
Copy link

hjzheng commented Dec 21, 2015

@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.

@lili21
Copy link

lili21 commented Feb 3, 2016

angular最佳实践那一部分,使用ADM方式定义。 有具体的例子吗?

@wangmeijian
Copy link

“module不支持运行时添加依赖”,踩过坑的路过,顺便奉上解决办法http://www.cnblogs.com/wangmeijian/p/5020788.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

15 participants