上一篇博客Javascript模块化介绍了前端的模块化的一些方案和历史,基于浏览器的AMD规范,我们尝试自己实现一个AMD Loader。
AMD主要是为了解决浏览器端的模块化,实现AMD Loader的话,以下的点要考虑到:
AMD Loader暴露 define和require其实我们主要要解决的点在于,如何处理Module的加载。Module加载要解决两个主要问题,一是对模块的引用(对path的处理);二是如何维护模块之间的引用关系。
首先来看看define的API接口:
define(id?, dependence?, factory)define的函数中id是表示当前模块的id,id可以是字符串或者一个绝对路径的字符串,这里规定不能出现相对id,例如’./myModule’或者 ‘../myModule’;id是可选的,如果没有提供id的话,默认id名是Module的在浏览器加载当前Module的url。
dependence,是一个字符串数组,字符串是依赖模块的url,可以是相对路径、绝对路径、字符串(如JQuery)。
所以,我们首先要解决的问题是,如何去规范化用户传进来的各种各样的url,例如,下面这些url的规范化:
./a/b/c//d --> a/b/c/d
./a/b/c/../../d --> ./a/d
./a/b/c/./d --> ./a/b/c/d
main/test?foo#bar --> main/test对于这些URl,我们可以写正则表达式去匹配,在用户传入模块的’url’,需要过一遍我们的正则,将其规范化,对于依赖模块,我们要基于当前Config的baseURL(如果在config中配置了baseUrl的话),拼成完整的url,让浏览器的script去加载,在加载完之后,将对应的script删除。
function loadScript(url, callback) {
var node = document.createElement('script');
var head = document.head;
node.setAttribute('data-module', url);
node.async = true;
node.src = url;
function onload() {
node.onload = node.onerror = null;
head.removeChild(node);
callback();
}
node.onerror = function(error) {
node.onload = node.onerror = null;
head.removeChild(node);
callback(error)
}
}如上,是使用script标签加载模块的一些代码,基本逻辑是:
创建script标签 ---> 加载模块源码 ---> 模块加载完成后触发回调函数
这里主要是利用script标签来做模块的加载,这里给script标签加上了async=true的标志,浏览器在解析HTML页面的时候,不会因为加载script阻塞住页面的解析,在script加载成功后执行script中的代码。
在使用AMD Loader定义我们的Module的时候,使用define(id?, dependence?, factory)的接口来定义我们的Module,在实现模块定义的时候,我们可以预想到有以下问题:
notify机制cache
如上,对于main Module有dep0, dep1, dep2三个依赖,这里我们在定义main Module的时候,将main Module中的每个依赖(在声明中只有url)实例化成Module,在加载main Module的时候,得先要加载我们的依赖Module,这里因为Module的加载是异步的,所以在实例化我们的依赖的时候,在每个Module中保存一个refs的数组,这个数组中保存着依赖这个Module的id,当前Module加载完成之后,在onload的callback中去通知当前模块load完成,然后当前Module会查看当前Module的refs中所有的Module,在refs中这些Module就会收集自己所有的依赖是不是都加载完成,如果加载完成了,当前的Module就加载完成了,否则就触发当前ref的重新load(为了触发其他未被加载的依赖的加载)。
解决了notify的问题,剩下的两个问题主要是对Module状态的维护,如下图,是整个Module的加载的状态转移图:
依照上图的状态转移图,Module Class的设计如下:
function Module(url, deps) {
this.refs = [];
this.depsUrl = getVaildUrl(deps);
this.depsModule = [];
this.STATUS = STATUS.INIT;
}
Module.prototype = {
constructor: Module,
fetch: function() {}, // 加载module的代码
save: function(depsUrl), // 检测修正依赖的url,并剔除已经被加载的module的url
resolve: function(), // 实例化每个依赖
setDependencies: function(), // 设置依赖当前模块的refs
checkCircular: function(), // 检测循环依赖
makeExports: function(), // 调用Module的factory函数并输出exports
load: function() {}, // 加载模块
}对于Module本身来说我们这里也需要维护一个状态,来表明当前Module的执行状态:
init --> fetch --> save --> load --> executing --> executed or error
AMD-Loader的完整代码 —> AMD Loader,代码实现仅为了解AMD模块加载的原理,并不能用在生产环境下。
兴趣遍地都是,坚持和持之以恒才是稀缺的