===============================================================================
Qomolangma OpenProject v2.0
类别 :Rich Web Client
关键词 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
DOM,DTHML,CSS,JavaScript,JScript
项目发起:aimingoo ([email protected])
项目团队:../../contributor.txt
================================================================================
注:有关本次发布的一些重要信息请参见:
http://blog.csdn.net/aimingoo/archive/2007/12/31/2005755.aspx
一、Qomolangma 2.0 Beta1
============================
Qomo的上次发布是在5个月前,此次的beta1主要包含对框架、兼容层和Builder系统的更新。有关界
面组件库、图形库的发布,大约要到beta2或beta3才会提供。
Beta1主要的更新包括:
- 修改了大量代码和builder工具,实现对IE5.0的完整兼容,兼容safari v2,部分兼容opera
- 对兼容层代码做了大量修改, 并调整了兼容层在system.js中的装载顺序
- 新增载入第三方代码的$import2()函数
- 实现接口委托
- 实现类声明中的通用的特性get/setter
- 更好的列举对象Enum()的实现,示例参考TestCase/T_Enum.html
- 分析JavaScript源代码语法的工具ParseLite, 修改自Brendan Eich的Narcissus,性能极大优化
- 一些小的实用工具函数:IsClass(), IsObject(), IsInterface(),HasInterface()等等
- 高性能的模式替换对象Pattern(),示例参考TestCase/T_StringReplacePattern.html
- 提供了一个正则表达式的性能分析工具:/Debug/TestCase/RegExpPerformanceTool.html
- 其它的一些小的修正,以及代码注释上的增改
下面对其中的一些较特别的更新做补充介绍。
二、函数$import2()
============================
$import2()是一个特殊的$imoprt()实现,它只适用于装载第三方代码的情况。beta1中它只在ParseLite
的实现中被使用了,但以后的代码中将会有很多的地方用到它。它的函数描述为:
------------------
function $import2(src, prepare, patch, condition);
- src: 装入的.js的url地址
- prepare: 在装入代码前面添加的代码,例如变量初始化等
- patch: 在装入代码尾部添加的代码, 用于向外部(例如Qomo)传出数据
- condition: 装入代码的条件, 不填写时为true
------------------
其中src与condition参数与它们在$inline()和$import()中的用法是一致的。这里特别讲述一下prepare
与patch。其中,prepare是简单的脚本文本,我们一般用它来初始化一些变量,以避免第三方代码包中
的变量泄露出来,与Qomo的全局变量冲突。例如:
------------------
// 第三方代码文本(文件名: test.js)
value1 = 100;
name = 'mySystem';
------------------
如果我们直接执行它,那么Qomo的全局变量中就会多出value1和name两个变量名。如果我们用下面的方
法来装载:
------------------
$import2('test.js', 'var value1,name;', '');
------------------
那么就不会有问题了。
但接下来,我们装载的test.js又能如何使用呢?比如这个test.js有一个类,或者对象,我们该如何来
使用它呢?这就需要用到patch这个参数了。不过在讲述它之前,先说明$import2()中对this引用的特
殊理解。
$import2()总是返回一个对象obj,如果this没有指向window,则使用this值作为obj;否则为obj新
建一个对象实例。因此,如果你试图传入一个对象并用它传出值,那么可以这样写:
------------------
var obj = {};
$import2.call(obj, 'test.js', ...
------------------
这样的技巧可以用来修改对象的原型,例如:
------------------
$import2.call(MyObject.prototype, 'test.js', ...
------------------
当然你也可以直接使用传出对象,而无需事先声明它。例如:
------------------
var obj = $import2('test.js', ...
------------------
然而这个对象(或this引用)传入之后该如何使用呢?一方面你可以为patch参数传入一个字符串做代
码块,这个代码块中可以直接使用this。另一方面,你也可以为patch参数传入一个函数,该函数在执
行时的第一个参数就是这个this引用。例如:
------------------
var obj = $import2('test.js', 'var value1,name;', function(_this) {
_this.value = value1;
_this.name = name;
});
------------------
这样一来,我们就可以在外部系统中得到这个对象obj,它的value、name则来自于第三方代码。当然,
其它的对象方法或类都可以用这种技术来从第三方代码中获取。
$import2()的一个实现实例,是Common/ParseLite.js。它从3rd/jsparse.js中获取了parse()和tokens()
两个方法。而这两个方法被用作ParserLite.prototype原型方法。这样一来,ParserLite()就既具有Qomo
的类的特性,又在对Qomo完全无扰的情况下,使用了第三方代码。
三、通用的特性get/setter
============================
通用get/setter其实是Qomo V1中就已经具备的一个特性,只是未被公开。这个特性适用于这样的情况:
某个类的许多特性的方法都基本一致,或者适合放在同一个函数中来实现,例如:
------------------
function MyObject() {
this.setName = function(v) {
if (!v) v = 'normal';
this.set(v);
}
this.setValue = function(v) {
if (!v) v = 'normal';
this.set(v);
}
// ...
}
------------------
那么你可以用下面的代码来完成相同的功能:
------------------
function MyObject() {
this.setValue =
this.setName = function(v) {
if (!v) v = 'normal';
this.set(v);
}
// ...
}
------------------
也就是让setValue()与setName()使用同一个特性存取函数。
为了让你在代码中能够区分调用来自于哪一个方法,Qomo在调用这个特性存取函数时,会多传入一个
参数,因此你可以写出如下的代码:
------------------
function MyObject() {
this.setValue =
this.setName = function(v, n) {
if (!v) v = 'normal';
this.set(v);
ExtObject['OnChange' + n]();
}
// ...
}
------------------
这个例子中ExtObject是一个假设的外部对象。而在上面的特性存取函数被调用时,例如:
------------------
obj.set('Name', ' - MyName');
obj.set('Value', ' - MyValue');
------------------
则下面的外部对象方法也将被调用:
------------------
ExtObject.OnChangeName();
ExtObject.OnChangeValue();
------------------
当然,你也可以在这里传入参数v,或者用switch()语句来识别变量n并加以处理.
最后,请注意一个细节,对于通常的set/getter来说,其声明方法是:
------------------
this.getName = function() { ...
this.setName = function(v) { ...
------------------
而对通用的特性get/setter来说,传入的特性名是追加在其后的:
------------------
this.getName = function(n) { ...
this.setName = function(v,n) { ...
------------------
不过两种方法实现的读取器的使用方法完全一致:
------------------
obj.get(n);
obj.set(n,v);
------------------
四、接口委托(Delegate)
============================
接口委托可能是本次(Qomo V2 Beta1)发布中最复杂的一项改动。在以前的文档中提及过它只是
暂时未被实现,也指出了未被实现的原因:没有应用。
在Qomo V2开发中,因为$import2的出现,我们遇到了一个问题:如果一个对象是由$import2()
装载的代码来实现的,而该对象又声明了接口,则该接口也必须要委托给第三方代码实现。例如
下面的代码中:
------------------
// 接口声明
IMyIntf = function() {
this.doAction1 =
this.doAction2 = Abstract;
}
// 类声明
function MyObject() {
Attribute(this, '3RdObject');
this.doAction1 = function() {
this.get('3RdObject').doAction1.apply(this, arguments);
}
this.doAction2 = function() {
this.get('3RdObject').doAction2.apply(this, arguments);
}
this.Create = function() {
// 1. 导入第三方代码
// 2. 用第三方代码创建对象并置'3RdObject'特性
this.set('3RdObject', aObject);
}
}
// 类注册,并声明该类实现IMyIntf接口
TMyObject = Class(TObject, 'MyObject', IMyIntf);
------------------
在这个例子中,除了基本的框架之外,MyObject必须用重复的代码来实现doAction1, doAction2, ...
等等类似的方法。事实上这些重复的代码并没多少特别的意义。所以我们将该接口的实现委托出去呢?
在新的Qomo版本中,使用下面的类声明过程可以做到了:
------------------
// 类声明
function MyObject() {
Attribute(this, '3RdObject');
this.Create = function() {
// 1. 导入第三方代码
// 2. 用第三方代码创建对象并置'3RdObject'特性
this.set('3RdObject', aObject);
}
function proxy(_this) {
return _this.get('3RdObject');
}
Delegate(_cls().Create, [
[proxy, ['IMyIntf']]
]);
}
------------------
Delegate()工具函数实现在Interface.js中,用于委托一个对象/类的接口,交由第三方实现。在这个
例子中,它被交由一个返回第三方对象的代理函数(proxy)——当然也可以在Delegate()中声明并使用
函数直接量。
除了使用代理函数之外,也可以简单的使用一个对象(或对象直接量)。例如:
------------------
var obj = {
doAction1 : function() { ... },
doAction2 : function() { ... }
}
Delegate(_cls().Create, [
[obj, ['IMyIntf']]
]);
------------------
Delegate函数声明为:
------------------
Delegate(consigner, confer);
------------------
其中consigner是委托者,在本例中是_cls().Create,由于_cls()是一个在类构造周期中使用的特殊函
数,用于返回类的一个引用(本例中是TMyObject),所以事实上委托者就是TMyObject.Create。而该函数
事实上直接指向构造器MyObject,而当一个接口被注册到构造器时,其子类和实例都将继承该接口。这
就与在Class()中声明该类实现IMyIntf接口的语义是一致的了。
上面函数声明中的confer是一个份委托协议。该协议总是一个数组,结构如下:
------------------
[
[proxy, [confer_item, ...]],
[proxy, [confer_item, ...]],
...
];
------------------
如前所述,其中proxy可以是代理对象或代理对象。而confer_item总是一个字符串,呈如下格式:
------------------
[<*|+|->]InterfaceName[.MethodName[:AliasName]]
------------------
字符串开始应当是一个修饰字符,如果缺省,则修饰字符为"+"。修饰字符的含义如下:
* 实现指定接口,或所有接口的所有方法
+ 实现指定接口/接口的指定方法
- 不实现指定接口/接口的指定方法
也就是一个包含和排除的规则集。这个修饰字符有两条优先规则:
1. 排除匹配(-)优先于包含匹配(+/*)
2. 指定匹配(接口.名称)优先于通用匹配( * )
这样,就包含了所有的委托关系。不在委托关系中的接口和方法,则默认由consigner自己来实现。
这个字符串其它的三个部分用于指定接口。包括:
InterfaceName : 接口名. 例如IInterface
MethodName : 方法名. 例如IInterface.QueryInterface
AliasName : 别名, 这是指实现者(proxy)中用来实现MethodName的名字.
如果MethodName缺省,则表明实现整个接口(的全部方法);如果AliasName缺省,则表明代理与
委托者使用相同的方法名。
对于类和构造器来说,委托关系与接口注册一样,也是可以被继承的。也就是说,一旦接口被声明
委托,则子类和对象实例都使用该委托关系来实现接口。另外,委托关系也是可以被覆盖的,这种
覆盖是confer_item的,也就是说可以可选地覆盖先前协议中的某些部分。下面的例子说明这种继
承与覆盖的关系。
------------------
function MyObject() {
Delegate(_cls().Create, [
[{
doAction1: function() { alert('doAction1') },
doAction2: function() { alert('doAction2') }
}, ['IMyIntf']]
]);
}
function MyObjectEx(){
Delegate(_cls().Create, [
[{
doAction2: function() { alert('doAction2 - MyObjectEx') }
}, ['IMyIntf.doAction2']]
]);
}
TMyObject = Class(TObject, 'MyObject', IMyIntf);
TMyObjectEx = Class(TMyObject, 'MyObjectEx');
// 测试代码
obj1 = new MyObject();
obj2 = new MyObjectEx();
intf1 = QueryInterface(obj1, IMyIntf);
intf2 = QueryInterface(obj2, IMyIntf);
intf1.doAction2();
intf2.doAction2();
------------------
需要注意的是,接口被委托实现并不表明对象也具有该方法。以上例来讲,intf1.doAction2()调用
成功,并不表明obj1.doAction2()也能调用成功。Qomo没有自动实现该对象方法的能力。
五、接口内部聚合(Aggregate)
============================
Aggregate在Qomo V2中有一种新的实现方案。Aggregate(聚合)的概念,是将多个接口实现在同一个
目标中。它在Qomo的早期版本中被用于构造器或函数的内部,用于表明该构造器或函数内部实现了
某个接口,而在它的外(方法)中不被表现出来。它的用法如下:
------------------
function MyObject() {
// 声明聚合
var intfs = Aggregate(cls().Create, IMyIntf, IObject, ...);
// 实现被聚合的各个接口
var intf1 = intfs.GetInterface(IMyIntf);
intf1.doAction1 = ...
intf1.doAction2 = ...
var intf2 = intfs.GetInterface(IObject);
intf1.hasEvent = ...
intf1.hasProperty = ...
...
}
// 声明MyObject()注册了某些接口
Interface.RegisterInterface(MyObject, IMyIntf, IObject, ...);
------------------
在Qomo V2中,在我们实现了Delegate之后,我们发现聚合在实质上也可以理解为一种委托。对于上例
来讲,我们可以用同样的委托代码来实现:
------------------
function MyObject() {
Delegate(_cls().Create, [
[{doAction1: ...,
doAction2: ... }, ['IMyIntf']],
[{hasEvent: ...,
hasProperty: ...}, ['IObject']],
...
]);
}
------------------
因此,我们重新实现了Aggregate()这个工具函数,在它的内部存在一个名为Interfaces()的构造器。
我们将Aggregate()的关系委托给该构造器的实例,并注册到接口系统中。这种新的实现方案比早期
的版本更加简单,但使用的方法仍然是一致——不必再重写以前的代码。
新的Aggregate()的实现方案演示了Delegate()的灵活应用:我们对Aggregate参数构造了一个代理对
象,并与该代理对象(自动地)创建了一份协议。不过,为此我们也稍稍地扩展了一下Delegate(),
使得confer_item中直接InterfaceHandle,也就是接口的内部句柄。因此,在不知道InterfaceName
的情况下,在Delegate()中也可以将InterfaceName填为一个整数值(InterfaceHandle)。不过目前
这只被用在Inerface.js内部,用于Aggregate的实现。
六、其它
============================
1. 关于IE5、safari和Opera的兼容
---------
在Qomo V2中将全面兼容IE5.0,不过你必须使用Builer生成一个兼容它的版本,而不能直接在IE5.0中
直接装载(未经编译的版本)和调试。
在Qomo V2中也完全兼容safari v2,需要留意的是,对于safari的早期版本并不兼容。这是因为safari
从v2才开始支持Function.caller。
是同样的原因,却导致我们不能完全兼容Opera:它不支持Function.caller。而我们经过讨论,也无法
在Opera上模拟出该效果,所以我们完成了除该项特性之外的全部兼容代码。然后,开始期待Opera的新
版本……
2. 关于parse分析的问题与应用
---------
Qomo V2的Common目录中新增了一个ParseLite.js,我们通过装载第三方项目Narcissus的代码,并为它
简单地注册了一个Qomo类,从而实现了这个工具。
不过,有两点要加以说明。其一,这是一个优化过的Narcissus项目,原来的Narcissus在处理稍大的代
码块时效率就迅速地级数下降,通过灵活地使用RegExp,Qomo避免了这些问题。以对150K源代码进行分
析为例,Narcissus需要450秒,而Qomo的ParseLite只需要~~EN~~不到8秒。
其二,ParseLite使用一个被裁剪过的Narcissus代码。没有execute部分,也去掉了一些扩展语法和异
常处理——ParseLite希望被分析的代码是没有语法错误。
所以ParseLite适合于一些特殊的语法分析,例如分析一个代码块中有多少个函数,或者一个特定片断
中的某个符号是何种语义。尽管它最终也返回一个语法树,但这个语法树不能直接用于Execute——它
少了一些东西。
关于ParseLite的使用,请参考Framework/TestCase/T_ParserLite.html
3. 接口对特性提供直接支持
---------
这是一个非常好的特性。在以前我们曾经声明过这样的接口:
------------------
INamedEnumer = function() {
this.getLength =
this.items =
this.names = Abstract;
}
------------------
但它的实现却相当麻烦,因为Qomo对名为"getXXXXXXXXX"的方法有限制,因此你必须这样写代码:
------------------
function MyObject() {
this.getLength = function() {
// 这里的代码是为Attribute写的
}
this.Create = function() {
// 这里的代码才提供给接口, 并通过访问Attribute才得到值
this.getLength = function() {
return this.get('Length');
}
}
}
------------------
在Qomo V2中,这个过程变得非常简单。对于接口来说,在QueryInterface()时,名字以"get/set"开
始方法名,将会被映射到"对象.get()"或"对象.set()"。除非对象自己实现了getLength,或者根本没
有get/set方法(例如不是Qomo对象)。因此,用户不需要为此多写任意一行代码。如下例:
------------------
IUserInfo = function() {
this.getName = Abstract;
this.getAge = Abstract;
}
function MyObject() {
Attribute(this, 'Name', 'MyName');
Attribute(this, 'Age', 30);
}
TMyObject = Class(TObject, 'MyObject', IUserInfo);
var obj = new MyObjet();
var intf = QueryInterface(obj, IUserInfo);
alert(intf.getName());
alert(intf.getAge());
------------------
Qomo V2.0 Beta 1下载
----------
http://groups.google.com/group/qomo/web/Qomo.V2.b1.zip
或从如下地址签出SVN:
----------
https://qomo.svn.sourceforge.net/svnroot/qomo/trunk