本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
上一节的同步模块应用范围多在服务端,那对于客户端的架构模式,我们通常选择异部模块模式,因为浏览器环境不同于服务器环境,在浏览器中对文件的加载时异步的。因此要使用未加载文件中某些模块方法时必然经历文件加载过程。
对未加载文件中的模块引用,同步模块模式是无能为力的,为了解决这个问题,我们一起研究下今天的课题。
异步模块模式
模块化:将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控,可维护,可扩展,提高模块的复用率。
异步模块模式—AMD:请求发出后,继续其他业务逻辑,直到模块加载完成,再执行后续的逻辑,实现模块开发中对模块加载完成后的引用。
大家一开始可能有个问题,我们上一节写的同步模块,如果使用到了还没加载的模块,那就加载一下不行嘛?比如下面:
//加载脚本文件
var loadScript = function(src) {
var _script = document.createElement('script'); //创建脚本元素
_script.type = 'text/JavaScript'; //设置类型
_script.src = src; //设置加载路径
document.getElementByTagName('head')[0].appendChild(_script);
//将元素插入到页面中
}
//加载 localstorage 文件
loadScript('localstorage.js');
//使用 localstorage 模块
F.module('localstorage', function(ls) {
// do something
});
上面代码即使加载了文件也获取不到,问题出在了 localstorage 上面。应为浏览器的文件是异步加载的,虽然一开始加载 localstorage.js 文件,不过在文件没有加载完之前,还是可以继续做其他事情的,并且上面代码对于文件什么时候加载完成无法获取,而同步模块模式会立即调用该模块,此时文件尚未加载完成,才最终导致加载不到。
所以我们需要一种新的模式,异步模块模式。
一部模块模式的创建和调用都通过一个方法,如创建 Event 模块,但要涉及 DOM ,使用的时候可以如下:
F.module('lib/event', ['lib/dom'], function(dom) {
})
需要注意的是,第一个参数( lib/event )是模块 id ,一定要对应文件路径,这样实现起来会成本更小。
具体实现需要闭包环境,如下:
//向闭包中传入模块管理器对象 F (~屏蔽压缩文件时,前面漏写;报错)
~(fucntion(F) {
//模块缓存器。储存已创建模块
}) ((function() {
//创建模块管理器 F ,并保存在全局作用域中
return window.F = {
};
}) ());
有了安全的闭包环境,下面要做的就是为模块管理器对象提供一个 module 方法。这个 module 方法集模块创建方法于一身,在这个方法中要遍历所有依赖模块,并判断所有模块都存在才可执行回调函数,否则加载相应文件,直到文件加载完成才执行回调。
/**
* 创建或调用模块方法
* @param url 模块 url
* @param deps 依赖模块
* @param callback 模块主函数
*/
F.module = function(url, modDeps, modCallback) {
//将参数转化为数组
var args = [].splice.call(arguments),
//获取模块构造函数(参数数组中最后一个参数成员)
callback = args.pop(),
//获取依赖模块(紧邻回调函数参数,且数据类型为数组)
dep = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : [],
//该模块 url (模块ID)
url = args.length ? args.pop() : null,
//依赖模块序列
params = [],
//未加载的依赖模块数量
depsCount = 0,
//依赖模块序列中索引值
i = 0,
//依赖模块序列长度
len;
//获取依赖模块长度
if(len = deps.length) {
//遍历依赖模块
while(i < len) {
//闭包保存i
(function(i) {
//增加未加载依赖模块数量统计
depsCount++;
//异步加载依赖模块
loadModule(deps[i], function(mod) {
//依赖模块序列中添加依赖模块接口引用
params[i] = mod;
//依赖模块加载完成,依赖模块数量统计减一
depsCount--;
//如果依赖模块全部加载
if(depsCount === 0) {
//在模块缓存器中矫正该模块,并执行构造函数
setModule(url, params, callback);
}
});
})(i);
//遍历下一依赖模块
i++
}
//无依赖模块
} else {
//在模块缓存器中矫正该模块,并执行构造函数
setModule(url, [], callback);
}
}
在 module 方法中有两个方法我们还没定义, loadModule 方法与 setModule 方法。接下来先实现 loadModule 方法, loadModule 方法目的是加载依赖模块对应的文件并执行回调函数。
我们需要分为三种情况:
①如果文件已经被要求加载过,需要区分已经加载完成还是正在加载,如果是加载完成,异步执行该模块的加载完成回调函数(详见 F.module 方法中对 loadModule 方法调用部分)。
②如果文件未加载完成,我们需要将加载完成回调函数缓存入模块加载完成回调函数容器中(该模块的 onload 数组容器)。
③如果依赖模块对应的文件未被要求加载过,那么我们要加载该文件,并将该依赖模块的初始化信息写入模块缓存器中。
var moduleCache = {
},
setModule = function(moduleName, params, callback) {
},
/**
* 异步加载依赖模块所在文件
* @param moduleName 模块路径(id)
* @param callback 模块加载完成回调函数
*/
loadModule = function(moduleName, callback) {
//依赖模块
var _module;
//如果依赖模块被要求加载过
if(moduleCache[moduleName]) {
//获取该模块信息
_module = moduleCache[moduleName];
//如果模块加载完成
if(_moudle.status === 'loaded') {
//执行模块加载完成回调函数
setTimeout(callback(_module.exports), 0);
} else {
//缓存该模块所处文件加载完成回调函数
_module.onload.push(callback);
}
//模块第一次被依赖引用
} else {
//缓存该模块初始化信息
moduleCache[moduleName] = {
moduleName: moduleName, //模块ID
status: 'loading', //模块对应文件加载状态(默认加载中)
exports: null, //模块接口
onload: [callback] //模块对应文件加载完成回调函数缓冲器
};
//加载模块对应文件
loadScript(getUrl(moduleName));
}
},
getUrl = function() {
},
loadScrript = function() {
};
上面的思路是执行模块对应文件加载完成回调函数是为了使 module 修改内部的依赖模块统计变量 depsCount ,直到所有依赖模块全部加载后顺利执行 setModule 。加载模块对应的文件时需要引用 loadScript 加载脚本方法与 getUrl 获取文件路径方法,接下来是实现这两个方法。
//获取文件路径
getUrl = function(src) {
//拼接完整的文件路径字符串,如'lib/ajax' => 'lib/ajax.js'
return String(moduleName).replace(/\.js$/g, '') + '.js';
},
//加载脚本文件
loadScript = function(src) {
//创建 script 元素
var _script = document.createElement('script');
_script.type = 'text/JavaScript'; //文件类型
_script.charset = 'UTF-8'; //确认编码
_script.async = true; //异步加载
_script.src = src; //文件路径
document.getElementsByTagName('head')[0].appendChild(_script); //插入页面中
};
之前说将模块 id 和 文件的路径对应,这样的对应关系可以让我们加载该模块对应的文件更简单。有时同页面一起加载的模块不会被其他模块引用,对于这些匿名模块我们有时候也不用添加模块 id 。
接下来,实现最后一个核心方法, setModule 方法。这个方法实现很简单,却很巧妙。表面上看,该方法是执行模块回调函数,但实质上,它做了三件事:
①对创建模块来说,当我的所有依赖模块加载完成时,需要使用该方法。
②对于被依赖的模块来说,其所在的文件加载后要执行该依赖模块(即创建该模块过程)又间接的使用该方法。
③对于一个匿名模块来说( F.module 方法中无 url 参数数据),执行过程中也会使用该方法。
/**
* 设置模块并执行模块构造函数
* @param moduleName 模块 id 名称
* @param params 依赖模块
* @param callback 模块构造函数
*/
setModule = function(moduleName, params, callback) {
//模块容器,模块文件加载完成回调函数
var _module, fn;
//如果模块被调用过
if(moduleCache[moduleName]) {
//获取模块
_module = moduleCache[moduleName];
//设置模块已经加载完成
_module.status = 'loaded';
//矫正模块接口
_module.exports = callback ? callback.apply(_module, params) : null;
//执行模块文件加载完成回调函数
while(fn = module.onload.shift()) {
fn(_module.exports);
}
} else {
//模块不存在(匿名模块),则直接执行构造函数
callback && callback.apply(null, params);
}
}
喘口气哈~
到这里我们一部模块管理器就完成了,接下来体验一下一部模块开发的乐趣吧。
首先我们在 lib/dom.js 中定义 dom 模块。对于 dom 模块来说,它不依赖任何模块。
F.module('lib/dom', function() {
return {
//获取元素方法
g: function() {
return document.getElementById(id);
},
//获取或者设置元素内容方法
html: fucntion(id, html) {
if(html) {
this.g(id).innerHTML = html;
} else {
return this.g(id).innerHTML;
}
}
}
});
接下来我们定义一个 event 模块,保存在 lib/event.js 文件中,不过由于为元素绑定事件要通过 id 获取元素,因此要引用 dom 模块。
F.module('lib/event', ['lib/dom'], function(dom) {
var events = {
//绑定事件
on: function(id, type, fn) {
dom.g(id)['on' + type] = fn;
}
}
return events;
});
最后我们在页面中引用 dom 模块与 event 模块,为页面中的 button 绑定事件,添加交互。
//index.html 页面中
F,module(['lib/event', 'lib/dom'], function(events, dom) {
events.on('demo', 'click', function() {
dom.html('demo', 'success');
})
});
总个小结
模块开发不仅解决了系统的复杂性问题,而且减少了多人开发中变量、方法名被覆盖的问题。通过其强大的命名空间管理,使模块的结构更合理。
通过对模块的引用,提高了模块代码复用率。异部模块在此基础上增加了模块依赖,使开发者不必担心某些方法尚未加载或未加载完全造成的无法使用问题。异步加载部分功能也可将更多首屏不必要的功能剥离出去,减少首屏加载成本。