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

@xufei 2015-01-22 06:23:54发表于 xufei/blog

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