Ruby's Louvre

每天学习一点点算法

导航

关于javascript模块加载的思索2

经几天思考,想到一个叫“文件与模块”的问题。我们的模块肯定写在一个JS文件中,这些模块又可以分为核心模块与外围模块。核心模块当然写在主文件中,它应该包含最重要的逻辑,加载器,列队,命名空间构造器等等。但如果一个文件只存在一个模块这也太浪费了,而且会导致请求法过多,因此出现多个模块“共生”于一个文件的情况。在主文件的那些非核心模块,我称之为内围模块。其他内围与外围没有什么区别,只是所在文件不同而已。不地为了方便起见,内围模块不要依赖外围模块!

但我们用script标签引用JS文件时,它就哗啦啦地执行里面的脚本,最主要的逻辑可以无所顾虑地得到解析。但对于内围模块,它们的逻辑是放到一个函数体中,控制流只能从它们上面掠过,触摸不了它里面的东西。这个模块名与回调函数与相关的配置将进入一个处理函数(下文称之为use),再放入一个处理列队。如果存在依赖,则检测依赖模块所在的文件有没有加载,没有就加载文件,如果文件已加载,则检测此模块已装配到框架的命名空间中,最后执行回调函数。

从上面分析可知,这里面的操作大体可分为几类:文件加载,模块装配与执行回调,它们只能依次执行。综观大多数类库框架,给出的解决方案就是这两种:动态script插入与Ajax回调解析。

  • 动态script插入,就是生成一个script节点,设置其目标src,然后插入head节点中。之所以不用document.write,那是插入到body中,而且还有许多缺陷,具体参看我这篇文章
  • Ajax回调解析,就是利用XMLHttp对象,将请求回来的responseText再全局解析。注意,是全局解析,要实现它就必须用到window.eval(标准浏览器)或window.execScript(IE),或者再搞一个script标签进行解析。可见这方法需要处理许多兼容问题,另搭上跨域问题……。

我的立场很明显了,使用第一种。但script标签关于回调的处理还是有许多问题。

                    var script = dom.genScriptNode();             
                    script.src = url
                    dom.head().appendChild(script);      
                    script.onload = script.onreadystatechange = function(){
                        if ((!this.readyState) || this.readyState == "loaded" || this.readyState == "complete" ){
                            if(!dom.done[name]){
                                alert("加载失败1")
                                dom.head().removeChild(script)
                            }
                             callback();
                        }
                    }
                    script.onerror = function(){
                        script.onload = script.onerror = undefined;
                        alert("加载失败2")
                        dom.head().removeChild(script)
                    }

如果我们的script标签所引用的JS文件不存在时,在一些标准浏览器下,会触发其onerror事件,但在IE下由于没有onload事件与onerror事件,我们不能判定是已加载成功,我们只有假设如果成功加载目标文件,dom.done.moduleName为true,如果失败,当然为undefined,进行!dom.done[name]为true,从而移除这个无用的script标签。这方法理应很完美,兼容IE与标准浏览器,可惜标准浏览器并不是石头一块,它们还是有差异。可恨的opera会在加载失败时抛出一个致命错误,这个连try catch也无回天之力了。因此这个url一定要绝对正确,为此我们要引入真实url机制。

无论是dojo,还是JSAN(早些年最负盛名的模块加载框架),或是YUI,更不用说using.js、require.js、packages.js等小众的类库,它们都拥有一种将模块名(包名)转换为url的机制。如:

"query"====>"http://localhost:3000/javascripts/dom/query.js"

http://localhost:3000/javascripts/我称之为basePath,它是核心模块所在的JS文件的路径,dom是强制添加的,所有外围模块文件必须在此,query为模块名。取JS文件路径的方法可参看我这一篇博文。为了应该极端情况,有时我们不得不放弃此游戏规则,框架就无法找到正确的url了,这时我们显式地指出其路径,方法是在模块名添加一个小括号,里面就是其真实url。

      var module = "dom."+item,url;
      //处理dom.node(http://www.cnblogs.com/rubylouvre/dom/node.js)的情形
      var _u = module.match(/\(([^)]+)\)/);
      url = _u && _u[1] ? _u[1] : dom.getBasePath()+"/"+ module.replace(/\./g, "/") + ".js";
      var script = dom.genScriptNode();
      script.src = url
      dom.head().appendChild(script);
      var scope = dom.namespace(module,true)
      //..........

因为模块与回调函数,在我的构思中都是同一个坯子出来的,它们都是同一个方法的回调函数。我把此方法命名为use,不过兼职YUI3的add与use的职责。比如,这是一个外围模块query:

//位于单独文件/dom/query.js中
dom.use("query",function(){
    arguments.callee._attached = true;
    dom.query = function(selector,context){
        context = context || document
        try{
            var els = context.querySelectorAll(selector);
            return dom.filter(els,function(el){
                return el.nodeType === 1
            })
        }catch(e){
            alert("你的浏览器不支持querySelectorAll")
        }
    }
},{
    use:["collection"]
});

它依赖于另一个外围模块collection:

//位于单独文件/dom/collection.js中
dom.use("collection",function(){
    arguments.callee._attached = true;
    dom.filter = function(array, fn, scope){
        var result = [],ri = 0;
        for (var i = 0,n = array.length; i < n; i++){
            if(fn.call(scope || array[i],array[i],i,array)){
                result[ ri++] = array[i];
            }
        }
        return result;
    }
    dom.each = function(){/**/}
    dom.map = function(){/**/}
    dom.keys = function(){/**/}
//.....
})

在网页中这样调用:

      dom.ready(function(){
        dom.use("query", function(){
          var els =dom.query("p")
          alert(els)
        });
      });

如何区分二者,因为回调函数是无穷尽地调用,而模块则不可以,否则可能修改了一些重要的配置,它们只能执行一次。我们需要用一些东西来标识它是模块。下面是我想到的一个方法:

dom.use("collection",function(){
    arguments.callee._attached = true;
    dom.filter = function(){/**/}
    dom.each = function(){/**/}
    dom.map = function(){/**/}
    dom.keys = function(){/**/}
//.....
})

那么当这个函数执行一次,它就有一个静态属性,如果下次它又出现在列队,我们检测它而跳过:

                if(!fn._attached){//如果是模块则只会执行一次
                    fn();
                }

对于文件也是这样,如果此JS文件已经加载过,我们就不用再加载了,因此我们可以使用一个hash来存放此消息。

dom.loaded.collection = true;
dom.use("collection",function(){
    arguments.callee._attached = true;
    dom.filter = function(){/**/}
    dom.each = function(){/**/}
    dom.map = function(){/**/}
    dom.keys = function(){/**/}
//.....
})

基本上就是这样。我最后回顾一些概念吧。核心模块,框架的重要组成部分,它当然不位于use函数中,相反,use函数,处理列队,特征侦测等重要的东西都是它的组成部分。内围模块,它与核心模块是位于同一个JS文件中,它不应依赖于外围模块。外围模块,它可以依赖于其他外围模块,由于它肯定是用核心模块与内围模块的东西组建而成,在这些东西在外围加载之时已经存在了,因此我们不需要再写出这些内部依赖。只需列出那些外围模块即可,因为它们所在的文件是否已加载还是未知数。处理列队,只是一个普通的数组,它里面的元素可以是模块名,模块本身与回调函数。完。

posted on 2010-05-15 12:58  司徒正美  阅读(2257)  评论(1编辑  收藏  举报