avalon自己实现了一套可被替换的模块加载系统(AMD loader)。具体什么是AMD loader可参看doJo官方博客关于AMD loader的翻译讲解,看完之后,再继续往下看,会比较清楚些。
模块加载系统可替换原理参见javascript 闭包暴露句柄,可替换的前提是前面加载的amd loader文件将define
和require
函数暴露给window对象。具体如何替换可执行
avalon.config.plugins.loader(false)//不推荐,但有效 //或者 avalon.config({loader:false})
当然了,如果更改了amd loader的话,可不要用avalon.config.plugins.text|css(url)
了。
avalon.config.nocache可用来配置是否去除缓存,测试的时候
avalon可通过配置avalon.config.[shim|paths],来加载一些不符合amd加载规则的数据源,例如加载csdn上的jquery等。
paths意味着地址。这里需要注意的是,paths的设定时,需要:
,也就是全路径,不要像这样www.abc.com
,而是http:www.abc.com
。
shim则是一个object。
//shim {//参考下面的数据结构 $moduleId:{ src:$url exports:$fn|$string // string说明加载的js文件会在window下存放引用,例如jquery, "$"==>window["$"] deps:... } }
模块加载所用到的数据先放出来,方便源码阅读推断。
//avalon.modules 存储模块信息 { $moduleId:{//默认值 id:$string//随机生成或指定 exports:$object|$string,//暴露出引用 deps:$array|$object,//依赖的部件 为$object时,会添加当前部件的加载状态,依赖的deps 又都会在avalon.modules下有各自的状态记录 state:$number//加载状态 2:加载过 1:正在加载 factory:$fn//模块本身,会被存放到factorys中,他的this指代window对象 args:$array//保存依赖模块的返回值 } } //factorys数组下的factory函数 factory.delay=$fn//检查依赖,延迟加载 factory.id=$string//用来debug用的
require(deps, factory, baseUrl)
require 默认id的生成是通过"callback"+setTimeout("1")
来实现UniId。
require 函数会先调用loadSource
遍历deps是否都加载完,没加载完的,会根据加载文件类型调用不同的函数去完成异步加载,并将其要加载的依赖放入loadings
存储,通过各种合适的时机(例如,完成一个js模块的加载时)调用checkDeps
来将loadings
处理掉,更新依赖状态,进而加载自己。
这里需要注意的是,factory的this,指代window对象(不知为何)。
动态加载划分为三种:js、css、text(文本),分别对应一个函数。并对url #?
后缀进行删除。
加载css时,前面添加 css! 。
css分两种加载,先介绍一个简单的,复杂的则是通过AMD loader加载的。加载方式是构造<link href='...' rel='stylesheet' id='$sepcialURL'>
并插入到head最上边。这个地方有个疑问是,css样式覆盖会按照后来覆盖前面的样式来吗?有待测试。
text的加载是通过ajax做的,并将结果赋值给exports,保存在avalon.modules下面。
js加载会有对前面提到的avalon.config.shim进行处理(说实在的这个处理只有看完源码后方能使用无误。纠结~),具体的加载功能由loadJS
函数来完成。loadJS
通过创建<script class='$日期'>
节点并插入到head上来加载js文件,当加载完成后,会将class属性改写掉,并将define
函数定义的factory函数和回调函数执行一下。如果加载失败了,写个日志呗。
define(id, deps, factory)
define的实现会牵扯到用户传参的循环依赖,例如:加载a需要先加载b,加载b又需要先加载a。所以上面的数据结构$moduleId
和deps
就变的十分有用了。根据这个参数的关系,就可找到是否存在循环依赖。
define还会调用require函数来实现JS文件的加载。
define还是require函数的属性值哦。innerRequire.define=function(){...}
ready!
是avalon内置模块,主要用来等待游览器扫完dom树之后,再执行。avalon.ready
也就是基于此的,上下avalon.ready
源码。
avalon.ready = function(fn) { innerRequire("ready!", fn)// require('ready!',fn);使用innerRequire是预防avalon的AMD loader被替代掉}
checkDeps
是用来检查存放在loadings模块的依赖是否都完成加载,如果依赖都完成了加载,而自己没有,则去执行自己的factory工厂函数。这个函数用到了loop来跳过双for循环break问题,可以拿来借鉴下。
function checkDeps() { //检测此JS模块的依赖是否都已安装完毕,是则安装自身 //只要loadings有一个 loop: for (var i = loadings.length, id; id = loadings[--i];) { var obj = modules[id], deps = obj.deps for (var key in deps) { if (ohasOwn.call(deps, key) && modules[key].state !== 2) { continue loop } } //如果deps是空对象或者其依赖的模块的状态都是2 if (obj.state !== 2) { loadings.splice(i, 1) //必须先移除再安装,防止在IE下DOM树建完后手动刷新页面,会多次执行它 fireFactory(obj.id, obj.args, obj.factory) checkDeps() //如果成功,则再执行一次,以防有些模块就差本模块没有安装好 } } }
源码很棒且注释很全,直接上源码。作者司徒正美还在博文getBasePath 函数中进行了详细的讲解,有兴趣的可以去了解下。
//getCurrentScript(true); function getCurrentScript(base) { // 参考 https://github.com/samyk/jiagra/blob/master/jiagra.js var stack try { a.b.c() //强制报错,以便捕获e.stack } catch (e) { //safari的错误对象只有line,sourceId,sourceURL stack = e.stack if (!stack && window.opera) { //opera 9没有e.stack,但有e.Backtrace,但不能直接取得,需要对e对象转字符串进行抽取 stack = (String(e).match(/of linked script \S+/g) || []).join(" ") } } if (stack) { /**e.stack最后一行在所有支持的浏览器大致如下: *chrome23: * at http://113.93.50.63/data.js:4:1 *firefox17: *@http://113.93.50.63/query.js:4 *opera12:http://www.oldapps.com/opera.php?system=Windows_XP *@http://113.93.50.63/data.js:4 *IE10: * at Global code (http://113.93.50.63/data.js:4:1) * //firefox4+ 可以用document.currentScript */ stack = stack.split(/[@ ]/g).pop() //取得最后一行,最后一个空格或@之后的部分 stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, "") //去掉换行符 return stack.replace(/(:\d+)?:\d+$/i, "") //去掉行号与或许存在的出错字符起始位置 } var nodes = (base ? DOC : head).getElementsByTagName("script") //只在head标签中寻找 for (var i = nodes.length, node; node = nodes[--i]; ) { if ((base || node.className === subscribers) && node.readyState === "interactive") {//subscribers="$"+(new Date-0) return node.className = node.src } } }
这个测试时关于加载顺序的。
<!DOCTYPE html> <html> <head> <script src="avalon.js"></script> </head> <body> <div ms-controller="hello"> <h1>Hello, {{name}}</h1> </div> <script> avalon.ready(function(){ var model = avalon.define('hello', function(vm){ vm.name = 'Avalon'; }); avalon.scan(); }) </script> </body> </html>
<!DOCTYPE html> <html> <head> <script src="avalon.js"></script> </head> <body> <div ms-controller="hello"> <h1>Hello, {{name}}</h1> </div> <script> var model = avalon.define('hello', function(vm){ vm.name = 'Avalon'; }); </script> </body> </html>
做完这个练习,我相信你一定会对dom加载有个新的认识。
整个实现,为factory函数依赖注入废了不少劲,不知道添上angularJS的annotate函数,会不会简单些。