扩展SeaJS模块定义中的module参数的应用示例

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

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

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

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

我们可以马上写出这三个模块,我们把它们放在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作为代号. 
Js代码   收藏代码
  1. define(function(require, exports, module){  
  2.     module.constructor.prototype.getName = function(){  
  3.         var alias = seajs._data.config.alias;  
  4.         var id = this.id;  
  5.         var name = id.substr(id.lastIndexOf("/")+1).split(".")[0];  
  6.         for (a in alias){  
  7.             if(id.indexOf(alias[a])===0){  
  8.                 name = id.split(alias[a])[1];  
  9.                 name = (a+name.split(".")[0]);  
  10.             }  
  11.         }  
  12.         return name;  
  13.     };  
  14. });  

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

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

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

最后我们实现module.extend方法,通过这个方法实现继承. 
Js代码   收藏代码
  1. define(function(require, exports, module){  
  2.     var Person = require("./person");  
  3.     var Stuff = function(name, id){  
  4.         Stuff.superclass.constructor.apply(this, arguments);  
  5.         this.id = id;  
  6.     };  
  7.     module.extend(Stuff, Person, {  
  8.         getId: function(){  
  9.             return this.id;  
  10.         }  
  11.     });  
  12.     return Stuff;  
  13. });  


然而还有一个限制,在写Person类的时候,需要显示的指明继承自Object 
Js代码   收藏代码
  1. define(function(require, exports, module){  
  2.     var Person = function(name){  
  3.         this.name = name;  
  4.     };  
  5.     module.extend(Person, Object, {  
  6.         getName: function(){  
  7.             return this.name;  
  8.         }  
  9.     });  
  10.     return Person;  
  11. });  


可以看到这几段代码都还不尽人意,而且只能在Chrome浏览器下看到这张图,不过这却是一个介绍module原型扩展的很好示例. 

为module添加meta信息 
前面的例子,只是将name作为了局部变量,让debug时可以看得更清晰,我们在更多浏览器,或者离线状态下获取这些信息,我们需要将这些信息显式的存储在module实例里.因为module和exports的双生关系.我们可以在需要的时候把这些信息存在module实例中,不需要的时候(比如生产环境)将这些附加信息撤掉.比如Java的interface,只对开发和编译时校验有意义,运行时并无特殊意义. 

于是又实现了一个setMeta方法,这个方法现在接受两个参数,第一个是type用来标明模块输出内容是"constructor","static","instance","abstract"还是"interface".如果是"constructor",第二个参数指明继承自哪个对象.如果是"instance"第二个参数指明构造器. 
 
有了这些信息,我们可以辅助生成系统类图,甚至辅助生成文档. 

这些信息可以在开发时发挥作用,但是真的可以在线上去除么?现在通过显示的调用,module.setMeta()来写入meta数据时无法轻易做到线上剔除的.主要的问题是我们为模块输出内容时使用的是"="赋值语句或"return"语句.这种API的扩展性具有一定的局限性.如果是通过一个方法来输出模块内容,如module.export(exportsObject);这样就可以方便的重写export方法,在开发环境和生产环境选用不同的export实现. 

更好的Logger 
大大小小的框架喜欢提供log方法,可是有一个问题,在chrome的控制台下,所有日志输出都是log方法定义的那一行.无法快速定位到关注的代码.如图: 

所以建议使用原生的console.log,对于没有console的浏览器,我们伪造一个console 
Js代码   收藏代码
  1. console = window.console || {  
  2.     log:function(s){  
  3.         //alert(s);  
  4.     }  
  5. }  

接下来,我们为所有module添加log方法, 
Js代码   收藏代码
  1. define(function(require, exports, module){  
  2.     var mp = module.constructor.prototype;  
  3.     mp.log = function(s){  
  4.         if (typeof s == "string") {  
  5.             s = "[" + this.getName() + "]:  " + s;  
  6.         }  
  7.         return s;  
  8.     };  
  9. });  
  10. //usage  
  11. console.log(module.log(s));  

这样的好处我们没有丢失行号,又可以给输出内容加上模块信息,继续扩展下去,可以指定哪些模块输出日志,哪些不输出.我的目标是把控制台改造成代行号的 YUI Console.如图: 
 
不过还是可耻的失败了,因为即使module.log返回空,也会打一行日志出来.如图: 

我们想只打出stuff模块的信息,其他模块的log信息仍留了一些空行,不过点击行号可以立刻定位到代码,这个却非常不错. 
另外大家可能觉得这样输出日志太麻烦,这就要看大家的真实需求了,同时也可以向Java程序员打听一下如果输出一行日志需要写多少代码.. 

总结一下 
今天的示例说到底都不太成功. 
三个示例都在使用的module.getName(),访问了seajs内置对象,且只考虑了alias情况. 
第一个对象监测示例,不得己动用了eval,而且只能应用于chrome下"寄生式"继承这一种继承模式. 
第二个meta信息示例,还无法方便的从生产系统中移除meta信息. 
第三个log扩展示例,更是因为会留空行,而无法把console改造的符合期望. 
但是这三个示例从三个角度展示了扩展module可以做到什么,大家可以发挥想象在各自的项目中有针对性的实现自己的功能. 
我们只要知道module和exports是一对双生,我们不方便修改exports中的业务对象,但却可以在module对象里添加对exports的描述,乃至对exports的操作. 
以上. 示例在此, 代码在此
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值