Qomolangma实现篇(二):命名空间和别名子系统的实现
================================================================================
Qomolangma OpenProject v1.0
类别 :Rich Web Client
关键词 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
DOM,DTHML,CSS,JavaScript,JScript
项目发起:aimingoo (aim@263.net)
项目团队:aimingoo, leon(pfzhou@gmail.com)
有贡献者:JingYu(zjy@cnpack.org)
================================================================================
一、NamedSystem 模块概要
~~~~~~~~~~~~~~~~~~
NamedSystem 是Qomo的可选载入模块。这个模块主要实现三个功能:
- 对$import()在路径识别上的增强
- Namespace 子系统的装载
- Alias 子系统的装载
// TODO: NamedSystem.js是firefox兼容的。
二、NamedSystem 模块的构成与载入
~~~~~~~~~~~~~~~~~~
命名系统分成上述的三个部分,但它们的重要性并不相同。
$import()为了对路径识别进行增强,加入了一个标准的JavaScript对象:Url()。此后通
过一个匿名函数的执行来实现对$import()中功能的重述。$import()的重述,以及Url()对
象的实现这两部分的功能,对于一般的系统来说都是必须的。
接下来NamedSystem 模块将载入namespace和alias子系统的模块。但这两个模块是可选的。
在Qomo系统中,不强制使用命名空间或相关的功能。而且事实上,在绝大多数的情况下,
Qomo的命名空间系统都是自维护的。
NamedSystem模块的代码结构:
----------
Url = function() {
// Url object的实现
}();
void = function() {
// $import()的重述
}();
// 命名空间和别名子系统的载入
$import('Namespace.js');
$import('Alias.js');
// 命名空间和别名声明
$import('Qomo.spc');
$import('Qomo.alias');
// more...
----------
三、Url对象的分析
~~~~~~~~~~~~~~~~~~
在一般人看来,对一个Url的解析是很简单的。但如果你看一下注册表中这个键的子键:
[HKEY_CLASSES_ROOT/PROTOCOLS/Handler]
你就不会觉得这是一件简单的事了。
这个键描述了能在IE中支持的地址协议。IE扩展了Url,使用URI来统一描述资源文件、本机
和网络上的内容地址。使得浏览器跟资源管理器、操作系统紧密集成在一起。
对于本机文件来说,你可以通过这样一种地址协议在IE中访问它(在IE中选“文件->打开”
菜单,选中该文件并确认之后,就可以在IE地址栏看见它了):
file:///c:/windows/ntbtlog.txt
你也可以在任何一个帮助文件(.CHM)上点鼠标右键,查看一个“属性”,就会得到这样一个
“URL”:
mk:@MSITStore:C:/WINDOWS/Help/ups.chm::/MS-ITS:pwrmn.chm::/pwrmn_ups_overview.htm
这此地址也要被Qomo的地址系统理解。因为他们都可以在IE里访问,也可以是HTML网页,当然
也就允许使用javascript和Qomo。
Url()对象对协议的识别使用了一个正则表达式/^(/w+)(://*)([^//]*)/
这个表达式可以取得协议格式(type)和host地址,然后Url()中将分析整个Url,并返回在对象
实例的属性中,这些属性表括:
----------
parse: function Url.parse() {
[qomo_core code]
}
URL: http://sourceforge.net:80/project/showfiles.php/list?group_id=157100&type=1
type: http
host: sourceforge.net
port: 80
query: /project/showfiles.php
path: /list
param: group_id=157100&type=1
params: [object Object]
----------
在上面这些属性中,parse()的属性显示是比较奇怪的。在javascript中,系统内置的函数作
为字符串显示的时候,源代码将被隐含。例如执行document.writeln(Array):
----------
function Array() {
[native code]
}
----------
Qomo中实现了这种“隐藏源代码”的效果。例如:
----------
Url.parse.toString = $QomoCoreFunction('Url.parse');
Url.toString = $QomoCoreFunction('Url');
----------
这样它们被显示出来的效果就类似于JavaScript系统的内置函数了。
在使用方面,Url()采用了于JavaScript内置对象RegExp()类似的设计。即可以将Url
作为全局对象实例使用:
----------
Url.parse('http://blog.csdn.net/aimingoo/archive/2006/02/13/597658.aspx');
for (i in Url)
document.writeln(i, ': ', Url[i], '');
----------
或将Url作为对象构造器使用:
----------
url = new Url('http://blog.csdn.net/aimingoo/archive/2006/02/13/597658.aspx');
for (i in url)
document.writeln(i, ': ', url[i], '');
// url.parse('http://sourceforge.net/projects/qomo/');
----------
四、$import()的重述
~~~~~~~~~~~~~~~~~~
在system.js的实现中,我们讲述到$import.get/set的实现是为了留备其它子系统对它
进行重述。而命名空间中单独处理了这一部分。
1. URL BASE
~~~~~~
通常情况下,我们会用document.URL或者window.location.href来取得当前网页的Url
地址。但问题是,这种情况下得到的,可能会有参数,例如.aspx调用后面的参数表。这
会给后面的分析带来麻烦。因此在Qomo中,采用了一种技巧来取得真实的BASE URL:
----------
// url base for current document
var BASE = function() {
var el = document.createElement('IMG');
el.src = '.';
return el.getAttribute('src', 1);
}();
----------
此后,Qomo创建了一个Url()对象,对BASE进行分析(parse),其中的query属性就 我们
所需要知道的:基于当前Host的绝对路径(docBase)。
2. 暂存引用(reference)
~~~~~~
$import操作get/set方法,使得外部代码中可以通过$import.get()来取得$import()内
部函数的引用。由于内核单元初始化结束后会调用$import.OnSysInitialized(),因此
我们甚至可以暂存一个get/set方法的引用:
----------
var $getter = $import.get; // 暂存get()方法的引用
var activeJS = $getter('activeJS'); // 暂存_sys.activeJS()的引用
// more...
----------
3. 添加_sys的内部属性
~~~~~~
在重述后的$import()中需要更多的特性。这些特性(最好被)集中表现在_sys内部对象
上。而$import.set()提供了这种可能性:
----------
$import.set('docBase', docBase);
$import.set('absBase', absBase);
// more...
----------
这样一些新的属性就被添加到_sys对象上了。这使在其后的其它模块对$import()进行
重述时可以访问docBase属性或absBase()方法。
4. 被重述的特性
~~~~~~
在NamedSystem模块中主要对transitionUrl()方法进行了重述。也就是说,NamedSystem
重新理解了$import(targetUrl)中的targetUrl参数。使得它完整支持以下的特性:
- targetUrl可以是基于system.js的相对路径. (仅在system.js单元)
- targetUrl可以是基于当前host的绝对路径. (缺省行为)
- targetUrl可以是基于当前.js的相对路径. (namesystem.js重述)
- targetUrl可以是命名空间/别名下的包. (Namespace.js重述)
五、命名空间(namespace)子系统
~~~~~~~~~~~~~~~~~~
1. 什么是命名空间
~~~~~~
至少是看起来,命名空间(系统)好象是一个了不起的系统。因为几乎现在的流行语言都
要支持它。好象不支持它的话,就算不上流行,也不会流行起来一样。
事实上,命名空间没有什么了不起。如果你只是想写一个类,或者一个可控制的类继承
树,那么你用不上命名空间。但如果你想整合几个不同的类库,或者一大堆的第三方组
件包,那么这些组件包中总可能存在两个同名的类。这种情况下,你就需要将不同的类
放在不同的命名空间里头,使得它们不相互冲突。于是,就需要就UI.Microsoft.Tree和
UI.Yahoo.Tree这样的命名空间存在了。
2. JavaScript中的命名空间
~~~~~~
高级语言的命名空间支持这样的一种特性,例如:
----------
$import(UI.Microsoft.Tree);
var aTree = new TDirectoryTree();
----------
这种情况下,系统会默认认为你在创建一个UI.Microsoft.Tree.TDirectoryTree的树。
也说是说,高级语言会将命名空间作为“作用域”的限定符来使用。而在JavaScript中,
作用域要么是函数内(或更内层),要么是函数外,你没有办法指定作用域在哪一个命名
空间里。——在上面的这个例子里,JavaScript会认为是在创建一个TDirectoryTree的
树。
JavaScript v1.3中不存在命名空间。但在更高版本的JavaScript中,例如JScript 8(.net)
中,或者在JavaScript v2中,就存在命名空间。——事实上,在JavaScript 2的规范
里,命名空间是类型,而且是第一类(first class)的。
由于在JavaScript v1.x中不存在命名空间的概念,而且作用域限定是JS解释器内部理
解的,因而不可能改变。因此JavaScript 1.x中(通过第三方代码实现)的命名空间,通
常只具有“扩展类继承树”的作用,而不具备作用域限定的作用。
3. 如何实现命名空间
~~~~~~
在JavaScript中实现一个(没有作用域特性的)命名空间是很简单的事。因为他事实上
是一个类的全名而已。那么这种情况下,一段简单的实现代码可以是这样:
----------
// 1. 命名空间的建立
var Qomo={};
Qomo.System = {};
Qomo.System.RTL = {};
// 2. 类, 构造器
function MemProf() {
// Object constructor...
}
// 3. 命名空间上的类
Qomo.System.RTL.MemProf = MemProf;
// 4. 使用命名空间
mem = new Qomo.System.RTL.MemProf();
----------
可见,(JavaScript中,)一般意义上的命名空间,只是一个类构造器的引用而已。
4. Qomo中的命名空间
~~~~~~
在Qomo中的命名空间除了上述的含义之外,还有另外一层意义,也就路径标识。例如
我们如果要使用这样的代码
----------
$import('Qomo.System.RTL.*');
// or
$import(Qomo.System.RTL);
----------
那么我们真实的意图,是要将RTL中的全部文件载入。也就是“包载入”的功能。
由于JavaScript不具有列(本地或远程)目录的能力,因为“包载入”需要一个描述包
内容的文件,例如package.xml。解析这个文件并逐一载入的功能并不复杂,但问题是
Qomo中的$import()是基于路径系统的,因此需要将Qomo.System.RTL翻译成一个URL路
径。
这种工作,在一般的JavaScript实现的框架里,都是通过RegisterNamespace来实现的。
这个RegisterNamespace()可以实现为一个全局的函数,也可以实现为一个命名空间的
方法,不一而足。但基本上的意思,就是将一个namespace与一个url path建立对照。
Qomo也需要建立这样一个对照。但因为Qomo并不强制使用命名空间,因此Qomo也不强
制使用RegisterNamespace的方法。取而代之的是“影射(map)”系统。
在$map()函数中,Qomo创建了一个私有、唯一的$map$对象:
----------
var $map$ = {
//mapper of all path
//0..n : dynamic properties with this.insert()
signpost : function(p) { ... },
remove : function(p) { ... },
insert : function(p, n) { ... }
}
----------
$map中会有一些0..n等数字为属性的,数字代表路径上长度。例如"/system/rtl/'长度
为12,那么他会在$map$[12]属性指向的对象中。
这里利用了JavaScript的自动类型转换。事实上,我们是在使用$map$['12']这个属性。
这种使用方式看起来象是数组,但$map$比数组“干净”:没有一些多余的方法或者属性。
这个技巧事实上是用path.length作为hash_key建立起了一个哈希表。
接下来,$map$['12']存放的是一个用直接量方式声明的对象:
----------
sp = {
paths: new Array(),
names: new Array()
}
----------
首先我不认为在JavaScript中会创建一个“多么巨大”的命名空间系统,其次我认为在使
用path.length作为hash_key之后,已经不会存在多少的hash碰撞了。因此在paths/names
对照中,我简单的使用了数组。
5. 路标
~~~~~~
请注意上面的对象使用的变量名sp(signpost)。我使用“路标(signpost)”来说明这个map
结点,有什么含义呢?
在一个name <-> path的映射系统里,我们可以发现一个现象:
----------
url1 = /Qomo/Component/Tree/NodeTree/
url2 = /Qomo/Component/Tree/
url3 = /Qomo/Component/
$map(Qomo.Component, url3);
$map(Qomo.Component.Tree, url2);
$map(Qomo.Component.Tree.NodeTree, url1);
---------
在这个对照中,我们发现其实没有必须存储全部的对照表,我们只存储Qomo.Component与
url3的对照,然后我们可以根据一种简单的关系运算,就可以得到其它的子空间所对应的
path了。
这种空间的管理方式,我称为"路标(signpost)":
---------
$map(Qomo.Component, url3);
$mapx('Qomo.Component.Tree.NodeTree');
---------
与$map()并不一样,$mpax()并不需要路径参数($map()第二个参数)。$mapx()根据字符串
向前查找,当到达Qomo.Component时找到一个有效的、已存在的命名空间。这时取出它映
射的路径url3。接下来就可以简单的建立出一个 name -> path 的对照:
---------
Qomo.Component --> url3
Qomo.Component.Tree --> url3 + 'Tree/';
Qomo.Component.Tree.NodeTree --> url3 + 'Tree/NodeTree/';
---------
我们可以发现这种关系是可以计算出来的(因而不需要存储在$map$)。而关键在于,我们
需要查找到“路标”:Qomo.Component。
因此$map$事实上并不需要存储全部的 name <--> path的映射。它只需要存储上面这种
关键的“路标(signpost)”。这样做的另一个好处是,我们如果将一个命名空间的物理
位置转移,那么我们也只需要改变它的路径,他的子空间和相关路径的关系并不需要改
变。
$map$.signpost()方法,用于通过路径(path)在$map$中查找一个最近的路标。事实上,
它返回该signpost上的namespace。——我不希望外部代码有机会改变$map$中的路标,
如果你要这样做,请通过$map$.remove()和$map$.insert()方法。
6. 命名空间到路径的运算
~~~~~~
简单的说,“路标(signpost)”系统用于“路径到命名空间”的检索。那么反过来如
何处理呢?
前面我们说到过,命名空间是一个对象直接量。我们确定了命名空间的含义之后,我们
也应该知道,命名空间是一个独立的系统,与程序代码本身的逻辑无关。那么,我们也
可以知道命名空间“对象”中的一些原生属性是没有实际意义的。例如constructor。
我们这里要使用constructor属性的唯一原因,只是因为它不会在“for .. in”循环中
被列举出来。事实上象toString()这样的“对象基本方法”都不会被列举出来。但只有
constructor在“不继承”的独立系统中没有确定的意义。
因此在Qomo系统中,有很多独立的系统将会使用constructor来存储一些关键属性,这
只是为了达到属性名的隐藏,以避免与其它第三方系统在属性名上的命名冲突。这些“
Qomo中独立的系统”包括命名空间、别名、多投事件和类方法。
在命名空间中,我们利用namespace_object.constructor来存储它实际指向的路径。也
就是说,“命名空间到路径的运算”只是简单的存取constructor属性值。
7. 小结
~~~~~~
在命名空间中,可以用$map()来建立一个命名空间,并映射到一个URL路径。这种映射
关系被作为一个路标保存在内部的$map$对象中。
"路径->命名空间"运算通过$p2n()来实现,其本质是查找$map$中的路标。
"命名空间->路径"运算通过$n2p()来实现,其本质是存取命名空间对象的constructor属性。
命名空间可以是虚的。这种情况下,它的constructor指向一个空字符串。
命名空间是可以通过$mapx()来扩展得到的,这种情况下它不需要在$map$中保存路标。
(最重要的,)在JavaScript 1.x中实现的命名空间不具有作用域的意义,他本质上是一个
对象构造器的全称、限定符,以及路径信息的映射。
六、别名(alias)子系统
~~~~~~~~~~~~~~~~~~
在Qomo中有一个并不十分成熟的别名系统。尽管它是可用的,但在使用之前,你应该注意
“对一个命名空间建立别名,并不会影响到子命名空间”。
除了这种限制之外,这个别名系统还是很方便的。你可以看一下Alias.js的实现代码:简
单而又快捷。哈哈。
别名事实上也是一个命名空间,只不过它的constructor指向另一个命名空间而已。关于这
种结构,在$n2p()的实现中已经做过处理,因此与原有的Namespace系统能很好的并存。
因此(作为一个示例),Qomo.alias演示了一个简单的别名声明。
---------
$alias('Qomo.RTL', Qomo.System.RTL);
---------
这将使得Qomo.RTL命名空间被创建,且可以作为Qomo.System.RTL的别名使用。但请留意,
这并不表明Qomo.RTL.MemProf也将是Qomo.System.RTL.MemProf的别名。——别名系统对子
空间无效。
这很大程度上降低了别名系统的价值。事实上,解决这个问题的方法很简单:
---------
Qomo.RTL = Qomo.System.RTL;
---------
——使用引用的方式来创建别名系统就可以了。但这可能为Namespace系统中的name-path
关系的维护带来更多的麻烦。因此,我(暂时地)放弃了这种技术。而仅在Alias.js的注释
里提及到了它。