日期:2014-05-16  浏览次数:20513 次

扩展SeaJS模块定义中的module参数的应用示例
近三四个月公司有两个比较大的项目在忙,没怎么更新博客.现在一个项目已进入平台开发期,另一个即将上线,接下来会多拿出时间进行一些技术总结.已经预定了月中懒懒交流会上的分享,也会写一系列博文出来.即将上线的这个项目是一个OPOA应用,上一篇博客"让Mustache支持简单的IF语句"和本篇都是这个项目的某个角落,它们有个统一的TAG:MagixJS,我会逐步揭开它.

SeaJS是我的同事玉伯开发的一套小巧且强大的Module Loader.我想前端的朋友多多少少会有耳闻,就不多说了.接触Java还算多些,在我看来seajs的module就像是Java的class.模块与模块之间有着依存关系,seajs会保证你在使用一个模块时,已经将所依赖的其他模块载入到脚本运行环境中.

在模块化的代码组织形式下,我们可以放心大胆的细粒度书写模块(详见我之前关于模块静态编译的分析).当前的OPOA项目已有近一百个模块.在开发过程中,我发现无论模块本身还是模块之间的依存关系,应该都可以更清楚的描述出来.比如:
  • 模块输出的是什么,构造器,静态对象还是某个的构造器的实例?
  • 为什么A模块依赖B模块,是因为A继承B,还是因为A是B的实例?
需求来了
看一个具体场景吧:
Person = function(name){}
Stuff = function(name,id){}//extend Person
Developer = function(name,id,skills){}//extend Stuff

Developer继承Stuff继承Person继承Object这样的关系,我们将每个构造器写成一个模块.我们用YUI,KISSY使用的"Parasitic Combination Inheritance"模式(详见"JavaScript高级程序设计第2版"6.2.5,YUI代码)来书写类,并将其封装成SeaJS模块形式,以Stuff为例,代码如下:
define(function(require, exports, module){
    var Person = require("./person");
    var Stuff = function(name, id){
        Stuff.superclass.constructor.apply(this, arguments);
        this.id = id;
    };
    extend(Stuff, Person, {
        getId: function(){
            return this.id;
        }
    });
    return Stuff;
});

我们可以马上写出这三个模块,我们把它们放在mptest文件夹下,加上seajs的全局alias指明mptest文件夹的http访问路径,那么我们就可以通过"mptest/person","mptest/stuff"和"mptest/developer"这三个模块模块名来使用它们.

但这三个模块之间的继承关系并非一目了然.比如我在chrome的控制台下查看一个developer实例.

图中两个"F",两个"Object"分别代表完全不同的四个东西,可我们很难简单识别出来,如果对象很多,层次复杂,这不易读,而这样的代码交给合作开发的partner,要费很多口舌.我们希望能够看到如下的这张图:

通过这张图,我们可以清晰的看出类之间的继承关系,不用唠叨,也不怕忘掉.在模块化背景下进一步深入下去,我们可以将框住的"Person","Stuff"处显示对应的模块名,那么一旦我们需要修改某个方法时,如Stuff的getId(),就能够快速找到代码所在文件stuff.js.如下


我就是想在监视变量的时候能够看到上图这样的结果.
我还想通过简单的类描述,能够用NodeJS脚本跑出一整个包的类图.
我还想再Chrome控制台实现YUI Logger的日志分类显示以及过滤功能.
这些都可以辅助提高开发调试效率,也让模块间结构更清晰.
可是我又不想直接在构造器上做扩展,比如为Person指定静态属性,Person.modname="mptest-person",坏处不细说.

扩展module解决问题
我把需求拿出来和玉伯一起讨论,发现玉伯同学也在偷偷的做类似的提取模块特有信息的事.最终发现模块输出的内容位于module对象的exports属性中,而同时模块对象已经拥有了id,dependencies属性.而module是一个普通object对象,那么如果我们把module由普通object改为Module类的实例,那么我们就可以在Module.prototype内扩展一系列公共方法来完成不同的需求.于是就有了seajs的这个issue.玉伯同学很快就在SeaJS v0.9.5版本中实现了它,而我则答应写今天这篇文章来介绍这个issue的具体应用.

首先我扩展了一个module.getName方法,可以根据module.id(即module地址)获取到module的shortname作为代号.
define(function(require, exports, module){
    module.constructor.prototype.getName = function(){
        var alias = seajs._data.config.alias;
		var id = this.id;
		var name = id.substr(id.lastIndexOf("/")+1).split(".")[0];
		for (a in alias){
			if(id.indexOf(alias[a])===0){
				name = id.split(alias[a])[1];
				name = (a+name.split(".")[0]);
			}
		}
		return name;
    };
});

(注:这个方法并不严谨,只特殊处理了alias中包含完整http地址的情况,仅做示意.)

然后在实现object.create(详见这里)的时候用了eval,把模块名取代"F"作为局部function变量名.
var object = function(o){
    var cn = this.getName().split("/").join("_");
    eval("function " + cn + "(){}" + cn + ".prototype = o;var r = new " + cn + "();");
    return r;
};

(注:这个方法依然很不好,不应该使用eval,但是尚未找到替代方案.关于eval再说几句,eval可能会影响到Js引擎对代码的自动优化,会影响到代码的压缩混淆效果,所以这个里的实现不应该用于生产环境,只做开发调试辅助.)

最后我们实现module.extend方法,通过这个方法实现继承.
define(function(require, exports, module){
    var Person = require("./person");
    var Stuff = function(name, id){
        Stuff.superclass.constructor.apply(this, arguments);
        this.id = id;
    };
    module.extend(Stuff, Person, {
        getId: function(){
            return this.id;