zoukankan      html  css  js  c++  java
  • 关于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文件中,它不应依赖于外围模块。外围模块,它可以依赖于其他外围模块,由于它肯定是用核心模块与内围模块的东西组建而成,在这些东西在外围加载之时已经存在了,因此我们不需要再写出这些内部依赖。只需列出那些外围模块即可,因为它们所在的文件是否已加载还是未知数。处理列队,只是一个普通的数组,它里面的元素可以是模块名,模块本身与回调函数。完。

  • 相关阅读:
    辅助随笔:因知识点不足暂时错过的题目
    NOIP2019翻车前写(and 抄)过的代码
    NOIP2019翻车前计划以及日记
    Luogu P3706 [SDOI2017]硬币游戏
    Luogu P5296 [北京省选集训2019]生成树计数
    Luogu P3307 [SDOI2013]项链
    Gaussian整数
    Problem. S
    LOJ6696 复读机 加强版
    数据库约束
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/1735375.html
Copyright © 2011-2022 走看看