zoukankan      html  css  js  c++  java
  • Qomolangma实现篇(一):内核载入模块system.js的实现

    ================================================================================
    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
    系统提供了充分的基础。

  • 相关阅读:
    快速幂模板
    部分有关素数的题
    POJ 3624 Charm Bracelet (01背包)
    51Nod 1085 背包问题 (01背包)
    POJ 1789 Truck History (Kruskal 最小生成树)
    HDU 1996 汉诺塔VI
    HDU 2511 汉诺塔X
    HDU 2175 汉诺塔IX (递推)
    HDU 2077 汉诺塔IV (递推)
    HDU 2064 汉诺塔III (递推)
  • 原文地址:https://www.cnblogs.com/encounter/p/2188715.html
Copyright © 2011-2022 走看看