关于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文件中,它不应依赖于外围模块。外围模块,它可以依赖于其他外围模块,由于它肯定是用核心模块与内围模块的东西组建而成,在这些东西在外围加载之时已经存在了,因此我们不需要再写出这些内部依赖。只需列出那些外围模块即可,因为它们所在的文件是否已加载还是未知数。处理列队,只是一个普通的数组,它里面的元素可以是模块名,模块本身与回调函数。完。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?