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