Qomolangma实现篇(一):内核载入模块system.js的实现
2006-02-13 01:16 乱世文章 阅读(221) 评论(0) 编辑 收藏 举报================================================================================
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)
================================================================================
一、system.js 模块概要
~~~~~~~~~~~~~~~~~~
system.js是Qomo的第一个载入模块。这个模块主要实现三个功能:
- 基本的$debug()函数
- 基本的$import()函数
- 内核子系统的装载
system.js是firefox兼容的。
二、内核子系统的构成与载入
~~~~~~~~~~~~~~~~~~
在Qomo中,所谓内核是指由直接在system.js中载入的模块构成的系统功能层。system.js
实现了$import()函数,并通过它装载以下模块:
----------
$import('Names/NamedSystem.js'); //命名空间管理
$import('RTL/JSEnhance.js'); //基于标准JS的增强特性
$import('JSUnit/debug.js'); //增强的调试输出
// more ... //调试分析相关功能(profiler/unit test等)
$import('RTL/error.js'); //错误和异常处理
$import('RTL/object.js'); //实现面向对象语言特性和基类TObject
$import('RTL/ajax.js'); //tiny ajax sub system
// more ... //其它内核级别的特性(SOA、Interface等)
----------
Qomo的内核是可裁减的:如果开发人员不喜欢使用命名空间,那么可以不载入NamedSystem.js;
或者根本就不使用增强的OOP特性,那么也可以不载入object.js。——换而言之,Qomo的
结构可以让开发人员重新组织自己的内核子系统,并在这个基础上发展自己的语言和框架。
Qomo实现这个特性的方法,就是使用“函数重载”和“功能重述”的技术。system.js中的
$debug使用了“函数重载”的技术,而$import()中的部分实现特性就利用了“功能重述”
的技术。
简单的说:“函数重载”是指在延后的代码中重写当前函数;而“功能重述”则是指在延后
的代码中对函数中的一个、多个特性重新描述(并代码实现)。——基本上来说,如果你有良
好的代码习惯,那么可以用“更完美的面向对象的设计”+“OOP的多态特性”来实现这两种
技术。然而,system.js是在一个很底层的、内核级别的实现,我不希望它变得庞大而低效,
所以使用了技巧来替代设计。
三、$debug()的分析
~~~~~~~~~~~~~~~~~~
在JScript系统中,提供一个调试期对象Debug,这个对象用于调试器控制台输入信息。如果
你使用C以及其它的高级语言编程序,你应该知道OutputDebugString()函数。而这个Debug
对象的作用就与它相同。这个对象提供了两个方法:Debug.write()和Debug.writeln()。
然而Qomo试图实现一个更有价值的调试输出子系统。例如向一个输出控制台发送对象,或者
用于记录效率分析信息等等。因此Qomo提供了$debug()函数。
然而作为基础模块system.js中的$debug并不提供上述的这些(完整的)特性。system.js中的
$debug()仅仅只是向document输出字符串信息。这与Debug.writeln()是一样的,只是输出信
息的目标不一样:Debug面向调试控制台,而$debug()面向window.document对象。
在system.js中的$debug()实现起来可以非常简单。例如这样:
----------
$debug = document.writeln;
----------
然而这样的代码在firefox中会出错。firefox对一些对象的方法做了保护,使得它不能被赋
值给JavaScript对象/变量,也不能反过来试图通过赋值来修改这些行为。因此在system.js
采用的最终代码是这样:
----------
$debug = function() {
document.writeln(Array.prototype.join.call(arguments, ''))
};
----------
这样就将$debug传入的参数arguments视作一个数组,并通过Array对象原型中的join()方法
连接成一个字符串,最后使用document.writeln()输出。
我们前面说到过$debug()使用了“函数重载”的技术。这是因为这里的$debug()事实上只被
随后载入的NamedSystem.js和JSEnhance.js使用。——如果在它们“加载的过程中”出了错
误、异常(或者出于调试的需要),就可以通过$debug()来输出信息。然而接下来:
- 第一步:在JSEnhance.js的未尾,会有一行代码将$debug置为空函数(NullFunction);
- 第二步:在debug.js的头部,有一段代码重写$debug()函数,实现自己的输出控制台。
这样就保证了在任何的代码中都可以使用$debug()函数来输出信息。而这个输出的表现会是
这样:
- 如果是在内核载入中,则向document输出错误信息;否则,
- 如果载入了debug.js模块,则会有一个输出控制台来显示信息或者对象(数据);否则,
- $debug()信息被屏弊,或由用户加载的第三方模块来承接调试信息或错误信息的输出。
开发人员可以自由地、安全地使用$debug(),而无需关心它怎么实现,或者如何输出。如
果不希望WEB浏览者看到它,只需要去掉debug.js(或者第三方的模块),而无需移除源代码
中的函数调用。
四、$import()的实现
~~~~~~~~~~~~~~~~~~
为了使得system.js等内核级别的模块不影响全局变量的定义,因此在内核的很多地方(当
然也包括$import()函数),使用了以下技巧来声明函数:
----------
$import = function() { // <--- 匿名函数1
var data= ...
function foo() { ... }
return function() { // <--- 匿名函数2
...
};
}();
----------
这样一来,foo()和data都声明在一个“匿名函数1”的内部,因此不会对今后代码中对全
局变量的命名造成影响(命名重复)。而$import()实际上是“匿名函数1”执行后返回的结
果:匿名函数2。——JavaScript中,“函数”(对象)可作为其它函数的执行结果返回。
重要的是,由于“匿名函数2”与data、foo()在同一个“上下文环境”中,因此它可以自
由地存取这些变量和方法。而外部、全局的其它代码就看不到$import()的实现细节了。
利用这种技巧,$import()实现了许多内部功能和信息的隐藏。其实现如下:
----------
$import = function () {
// for firefox only
var _SYS_TAG =
var _MOZ_TAG =
var _CHARSET =
// 使远程获取的脚本
var toCurrentCharset = ...
// 通过检测当前的网页字符集,来确定.js文件使用的编码
var _uu = ...
// 在firefox以及IE的不同版本下取HTTP连接对象(HTTPRequest)
var getHttpConnect = ...
// 在$import()中使用的一个唯一的http connect
// (脚本执行是有先后的,所以没有必要使用异步的HTTP连接)
var _http = ...
// 是否使用XMLHTTP来取脚本代码。如果为false,则使用<script>标签载入
var _xml = ...
// 通过_http连接取代码,并转换编码的函数
function httpGet ...
// 取当前正在运行的脚本URL
// (例如system.js的URL,实现使用文件相对路径来$import()的特性)
function activeJS ...
// 重要的、在后期可能使用或“重述”的系统信息
var _sys = ...
// 一个activeJS的栈,用于实现“在$import()的代码中再调用$import()”的特性
var _stack = ...
// 远程读取、装载指定src的脚本并执行
var _load_and_execute = ...
// 向外返回的函数
function _import(src) {
/* Qomo Core System.. */
_load_and_execute( src );
}
// 其它代码(参见后文中“_sys对象的价值”)
// ...
return _import;
}
----------
下面我们逐一讲述其中的主要功能:
1. 网页字符集、unicode及其解码
~~~~~~
在ajax系统中,一个很重要的问题就是编解码的问题。因为不管是Microsoft的XMLHTTP控件,
还是firefox中的XMLHttpRequest对象,都将远程获取的内容默认识别为Unicode编码。XMLHTTP
控件默认通过远程内容中的前导字符来识别Uniocde的编码方式。因此在不特别指明的情况下,
XMLHTTP可以正确的解析以下编码方式的远程内容:
----------
var _uu = _CHARSET in {
'utf-8': null,
'unicode': null,
'utf-16': null,
'UnicodeFFFE': null,
'utf-32': null,
'utf-32BE': null
};
----------
如果XMLHTTP不能通过前导字符来解析编码,那么它就默认远程内容使用了UTF-8的编码格式。
然而这只是说“远程内容”的格式(例如.js文件使用的编码存储格式)。大多数情况下,我们会
在网页中用如下标签来描述“当前网页”的编码:
----------
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
----------
在没有这个HTML标签描述的情况下,IE会使用当前的默认设置来给网页解码并显示。这种情况
下,document.charset将会置为“_autodetect_all”,或者你在IE菜单“查看->编码”中选
择的字符集。在charset=="_autodetect_all"时,可以通过存取document.defaultCharset来
得到解码时选择的字符集。
我们看到一个问题:“当前网页”解码与“远程内容”解码所依赖的字符集设定并不一致。
事实上,麻烦不仅于此。在JScript引擎中理解的字符串等内容,使用的也将会是unicode字
符集。这一点,无论.js文件编码格式是什么,或者网页编码格式是什么,都不会被改变。
也就是说,在一个charsett=gb2312,且.js文件使用gb2312编码的系统中,你使用escape()
或unescape()都将会在一个unicode环境中进行字符串编解码。更有甚者,你即使强行指定了
一个字符串的解码方式,它最终显示在网页上的时候,也不会如你所愿。例如:
----------
// 字符串"这是一个测试"的gb2312字节码
var s1 = '%D5%E2%CA%C7%D2%BB%B8%F6%B2%E2%CA%D4';
// 解码
var s2 = unescape(s1);
// 显示
document.writeln(s2);
----------
这段代码在utf-8或gb2312字符集的网页上显示都不正常。
在Qomo中$import()函数的解码基于一个假设:“.js文件的‘远程内容’与‘当前网页’
必然使用相同的字符集”。——必须说明的是,这是在一个封闭环境中的理想情况。如果
你试图用$import()读取RSS的内容,你可能会必须面临“在gb2313网页中去处理utf-8编
码的RSS数据”这样的问题。因而你应该清楚:内核一级的$import()主要用于处理Qomo系
统(及扩展功能)的模块载入,其它的“远程内容”应该交由更复杂的ajax系统去做。
因此Qomo认为:远程跟当前网页采用相同编码,因此在网页字符集为unicode的情况下,
远程内容不需要解码,否则应当从XMLHTTP所(错误)理解的unicode转换为当前字符集。这
个转换依赖于当前网页字符串的设定,也就是$import()内部的_CHARSET变量的值。
Qomo在$import()中实现了解码函数:toCurrentCharset()。解码函数只实现了对gb2312
字符集的处理,如果需要其它(非unicode)的解码,则需要修改toCurrentCharset()中的
部分代码。
Qomo的解码函数最初实现gb2312字节码的处理时,借鉴网上流传很广泛的一个bytes2BSTR()
函数,实现了改良版本的vbs_JoinBytes()。在vbs_JoinBytes()函数中减少了字符串连接
和长度识别的次数,使效率大为提高。但在最终实现这个功能时,借鉴Hutia、bjhaoyun
在“经典论坛”中公布的、使用unescape()/escape()函数来处理编码字符串的技巧。由于
大量优化了代码,新的toCurrentCharset()比bjhaoyun提供的代码有30%左右的性能提升。
这几种解码方案中,toCurrentCharset()与bjhaoyun的reCode()采用相同的方案,但整体
性能提升30%。在通常情况下,toCurrentCharset()比vbs_JoinBytes()快3倍以上;在以
英文内容为主的情况下,可以快近10倍;但在中文字符量非常多(例如全中文文本)的情
况下,vbs_JoinBytes()的性能表现会极佳,甚至会比toCurrentCharset()快50%。
由于$import()主要处理的主体内容是英文代码.js脚本,因此选用了toCurrentCharset()
作为内置的解码函数。关于其它几个解码函数,可以参见测试网页T_DecodeUnicode.html。
(目前,)Qomo没有为firefox中使用XMLHttpRequest对象载入的内容提供解码函数。
2. XMLHTTP载入与<script>标签载入的区别
~~~~~~
如果XMLHTTP对象不能创建,或者无法正常处理编码。Qomo中提供了后备方案,也就是使
用<script>标签来载入模块及其它远程内容。
然而这两者原本是不能完全替代的,因此有一些差异之处必须补充说明。
首先XMLHTTP载入的内容存放在XMLHTTP对象(例如Qomo中的_http)的responseBody属性中,
这是一个以Byte为基础类型的SafeArray数组,而JScript只能处理以Variant为基础类型
的SafeArray。所以Qomo中调用VBScript的CStr()来使它变成字符串,然后进一步地交由
toCurrentCharset()处理。
——然而如果使用<script>标签来载入,那么这整个的解码过程就不需要了。因为<script>
可以指定charset属性,也可以直接使用“与当前网页相同”字符集的脚本文件。
如果仅这样看,<script>会比XMLHTTP好。但事实上XMLHTTP具备的另一项优势让<script>
望尘莫及。
使用异步方式,XMLHTTP载入的内容可以被立即执行。因此在这个例子中:
----------
<script>
$import('1.js');
$import('2.js');
foo_in_js1();
</script>
----------
前两行的1.js和2.js被立即载入并执行了,因此在1.js文件中的foo_in_js1()可以得到
执行。而在下面的例子中:
----------
<script>
document.writeln('<script src="1.js"><', '/script>');
document.writeln('<script src="2.js"><', '/script>');
foo_in_js1();
</script>
----------
document.writeln()向网页写入的内容会出现在</script>标签之后。因此,1.js和
2.js会在当前的脚本块被全部执行完之后,才被载入、解析并执行。——这也意味着
函数foo_in_js1()调用不会成功。
很显然,我们在一个大的框架系统中,会利用下面这样的代码来说明当前模块(或单
元)的依赖性:
----------
$import('/OS/Win32/FileSystem/*');
$import('/OS/Win32/UI/*');
// some code ...
----------
这种情况下在“some code”执行前FileSystem和UI模块就应该是被载入、执行过的。
而我们已经看到<script>并不支持这种特性。
因此Qomo内核中使用<script>来替代$import()仅仅是权益之计,它不能完成$import()
的全部工作。——但是在一些简化的、小型的、经过定制Qomo系统中,他仍旧是可用
的。只不过要注意XMLHTTP与<script>之间的这种差异,以及这种差异带来的负面影响。
3. execScript()与eval()的不同表现
~~~~~~
JScript中有window.execScript()方法,但JavaScript规范中却没有它。因此firefox
并没有实现一个execScript。另一个与之相近的是Global.eval()方法。
在IE的JScript中,eval()执行一个字符串并返回结果。在执行时,使用的是调用函数的
上下文环境。因此如果函数A中调用了eval(Str),那么字符串Str中的脚本代码可以使用、
修改和影响函数A中的局部变量。而window.execScript()将直接使用全局上下文环境,
因此,execScript(Str)中的字符串Str可以影响全局变量。——也包括声明全局变量、
函数以及对象构造器。
因此我们在用XMLHTTP来远程地取得.js文件的内容之后,我们就可以利用execScript()
来执行它。这种执行与在<script>标签中的执行效果是一致的。
从JavaScript的约定来说,Global.eval()不具有在全局的上下文环境中执行的能力。在
做一个偶然的代码测试时,我发现firefox中的eval()存在一个奇怪的特性:
- 如果在函数中使用window.eval()来执行,则使用全局上下文环境;
- 如果使用eval()来执行,则使用当前函数的上下文环境。
我不确知这是FireFox为ajax而提供的语言特性呢,还是它一个JavaScript实现上的BUG。
但我测试过的几个版本都呈现这种效果。因此$import()中我使用了window.eval来替代
window.execScript(),以实现firefox版本的Qomo内核。
关于这个特性请参见测试网页T_eval.html。
4. 载入路径与activeJS的关系
~~~~~~
在Qomo系统中载入一个.js时,采用比较灵活的路径定位策略:
- 如果src以"xxxx://"的形式开始,则使用“完整路径”定位;
- 如果src以"/"开始,则使用以当前document所在网站的根路径开始的绝对路径;
- 否则使用相对路径定位。
但接下来的问题就比较麻烦了。如果网页的URL是“http://site/sub/a.html”,而
Qomo系统被部署在"http://site/Qomo/"路径上,则可用如下两种方式之一在a.html
中载入system.js:
----------
<script src="../Framework/system.js"></script> <!-- 之一 -->
<script src="/Qomo/Framework/system.js"></script> <!-- 之二 -->
----------
因为XMLHTTP也使用当前网页来做相对定位,因此在这方面他与<script>对象相同。
那么显然我们在system.js中导入NameSystem.js应该使用下面这样的代码:
----------
// $import()使用XMLHTTP来实现
$import('../Framework/Names/NameSystem.js'); // 方法一
$import('/Qomo/Framework/Names/NameSystem.js'); // 方法二
----------
各个.js中都要使用当前网页来做相对定位,这导致了系统中的脚本很不灵活,编写
代码时要留意其它.js所在位置,目录转移时也不方便。——当然你也可以使用"/"开
始的绝对定位,但如果这样,Qomo在应用系统中的可部署性就很差了。
因此Qomo对载入路径的理解是基于“当前.js文件”的。这样一来,在system.js中
就可以这样来导入NameSystem.js:
----------
// system.js位于/Qomo/Framework/路径上
$import('Names/NameSystem.js');
----------
而在NameSystem.js中如果要导入同样位于Names目录中的A.js文件,则只需要:
----------
$import('A.js');
----------
为此,Qomo在$import()实现了一个_stack数组变量。它被当做一个后入先出队列,
以保证curScript中总是存在当前正在执行的.js文件的URL。这样$import()就可以
据此来计算“正在执行的.js中新导入的.js的相对路径”。
麻烦并没有解除。因为Qomo对系统的理解是“可拆解”的,因此它会允许下面这样
的代码:
----------
<script src="../Qomo/Framework/system.js"></script>
<script src="../Qomo/Controls/Components.js"></script>
<script src="../Qomo/DB/DB.js"></script>
<script>
$import('../Qomo/Common/Tools.js');
</script>
----------
在这样的一个系统中,system.js、Components.js和DB.js中所理解的“当前路径”
都不一样。因此我们必须有办法来知道$import()当前正在哪一个.js文件中执行,
并取得它的src。
函数activeJS()用于取得由<script>导入的.js文件的URL,然后在.js中就可以使
用相对路径来载入其它.js文件了。
在IE中有机会取到“当前.js的路径”。在使用中我发现<script>对象的readyState
属性可以帮助我们来实现这个需求。简单的说,.js文件执行时,script.readyState
的值将会是"interactive"。如果我们列举所有的<script>标签,则可以找到这个
script对象,从而得到src的值。
activeJS()利用这个特性来实现。但firefox中DOM的Script对象不具有readyState
属性,因此firefox部分的代码采用了识别文件名"system.js"的方法来实现。——
但需要注意的是,我没有办法为Qomo在firefox中提供与IE一样的特性。它们之间的
差异表现在:
- (除非在system.js中,)$import()在firefox中不能使用相对路径的特性
- 如果修改system.js的文件名,则firefox在system.js也不能使用相对路径
最后,在一个普通的<script>脚本块中使用$import(),它将会以当前的document的
路径作为计算相对路径的基础。这时,activeJS()返回空串。——正好script.src
也是空串。
4. 为什么不是命名空间
~~~~~~
Qomo支持但不强制使用命名空间。这是Qomo如此复杂地实现$import()的原因。因
为在一个支持“命名空间注册”的系统中,可以这样来做:
----------
// 1. in system.js
Names.registerNamespace('/Framework', 'currentPath');
// 2. in my_script.js
$import('/Framework/Debug/debug.js');
----------
这样,由于命名空间的存在,系统的确可以快速地反映模块的位置和相互关系。然
而这种通过registerNamespace()手工注册的形式,导致用户必须强制使用命名空间。
——尽管这没有什么不好,也的确是firefox上可以采用的最佳方式。然而我还是在
Qomo中实现了自注册的命名空间管理子系统,这使得在大多数情况下,命名空间都可
以通过$import()调用时的路径关系来自动获取和计算。
即使不加载命名空间模块,Qomo系统也能正常的运行。这是Qomo的一个特点。尽管这
个特性在firefox中不能被实现,然而我还是为“喜欢快捷轻巧的内核”的开发者们提
供了一种可能的选择。
不过关于NameSystem.js的具体实现我们以后再讲。今次我们讨论的,只是system.js。
5. _sys对象的价值
~~~~~~
在Qomo的内核中,一部分代码是可被外部使用的。例如解码用的toCurrentCharset()
以及用于导入.js文件的、异步的XMLHTTP对象。
然而$import()封装了这些细节。在Qomo对代码的理解里面,是“没有必要,就不要
公布”,这样尽可能地少占用一些全局的变量名。
那么有价值的资源能如何被使用呢?Qomo在$import()函数声明了对象_sys,:
----------
var _sys = {
// 读取这些内容可以了解$import()的运行情况
'scripts': {},
'curScript': '',
// ajax kernal
'httpGet': httpGet,
'httpConn': getHttpConnect,
// decode for XMLHTTP.responseBody
'bodyDecode': toCurrentCharset,
// 取当前正在执行中的脚本的src
'activeJS' : activeJS
// ...
}
----------
然后为$import实现了一些用于存取这个对象的方法:
----------
_import.get = function(n) {
return eval('_sys[n]');
}
_import.set = function(n, v) {
return eval('_sys[n] = v');
}
_import.OnSysInitialized = function() { ... }
----------
这样,在Qomo中就可以写下面这样的代码来使用_sys对象了:
----------
var httpGet = $import.get('httpGet');
var str = httpGet('http://www.sina.com.cn/');
alert(str);
----------
而$import.set()的提供,就与我们在最前说到的“功能重述”技术有关了。
因为$import.set()修改的是_sys对象所存放的“内部功能入口”,因此可以
通过调用set()方法来“重新描述”这些功能入口的实现方法。这些能被重述
的内容取决于_sys对象公开了哪些内容。在Qomo中,这些是可以被重述的:
----------
var _sys = {
'transitionUrl': function(url){ ... }
'srcBase': function() { ... }
// ...
}
----------
一些人应该已经注意到$import()并没有实现“导入包或命名空间”这样的功能。
而这里公开这些功能入口,就是使得它们可以被“重述”:如果在NameSystem.js
中重述transitionUrl()和srcBase()的实现,那么就可以在不修改$import()的
情况下,支持命名空间和包。
然而这些“可重述”的特性仍然是与内核直接相关的。因此$import()还公开了
一个事件OnSysInitialized(),并在system.js的最未尾激活了这个事件。——
这个事件的响应代码所做的工作,就是为$import()清除set()/set()等这些方法。
通过$import.get()/set(),我们可以在system.js所导入的其它.js中为$import()
进行重述,也可以利用$import()已经实现过的特性和代码。而在system.js载入并
执行完成之后,这些预留给系统内核的功能就随着OnSysInitialized()的触发而被
清除了。
system.js模块实现了一个具有张力和包容性的载入框架,为后面实现可裁剪的Qomo
系统提供了充分的基础。