因为笔者所在的团队使用fis3打包工具搭配modJS来解决js模块化,而且最近也在研究js模块化方案,故写下这篇文章来解读modJS的实现细节。javascript
限于笔者水平,若是有错误或不严谨的地方,请给予指正,十分感谢。css
首先,来看一下modJS的简介以及使用方法:html
modJS是一个精简版的AMD/CMD规范,并不彻底遵照AMD/CMD规范,目的在于但愿给使用者提供一个相似nodeJS同样的开发体验,同时具有很好的线上性能。java
在日常开发中,只需写factory中的代码便可,无需手动定义模块。打包工具fis3会自动将模块代码嵌入factory的闭包里。 factory提供了3个参数:require, exports, module,用于模块的引用和导出。node
典型的例子git
// a.js 文件
define('js/a', function(require, exports, module) {
function init() {
console.log('模块a被引用')
}
return { init: init }
// or
// exports.init = init
// or
// modules.exports = { init : init }
})
复制代码
和NodeJS里获取模块的方式同样,很是简单。由于所需的模块都已预先加载,所以require能够当即返回该模块。github
典型的例子json
// index.html 文件
<script src="./mod.js" type="text/javascript"></script>
<script src="./js/a.js" type="text/javascript"></script>
<script type="text/javascript">
require('js/a').init();
</script>
复制代码
考虑到有些模块无需在启动时载入,所以modJS提供了能够在运行时异步加载模块的接口。ids能够是一个模块名,或者是数组形式的模块名列表。当全部都加载都完成时,onload被调用,ids对应的全部模块实例将做为参数传入。若是加载错误或者网络超时,onerror将被触发。超时时间经过require.timeout设置,默认为5000(ms)。api
使用require.async获取的模块不会被打包工具安排在预加载中,所以在完成回调以前require将会抛出模块未定义错误。数组
典型的例子:
// index.html 文件
<script src="./mod.js" type="text/javascript"></script>
<script type="text/javascript">
require.async('js/a',function(mod){
mod.init()
},function(id){
console.error("模块" + id + "加载失败")
});
</script>
复制代码
经过require.resourceMap(obj) 解析模块依赖树,并获取模块对应的url。由打包工具自动完成。
典型的例子
// resource_map.js 文件
require.resourceMap({
"pkg": {},
"res": {
"js/a": {
"url": "js/a.js",
"type": "js"
},
"js/b": {
"url": "js/b.js",
"type": "js"
},
"js/c": {
"url": "js/c.js",
"type": "js",
"deps": ["js/a", "js/b"]
}
}
})
复制代码
异步加载脚本文件,不作任何回调
异步加载CSS文件,并添加到页面
建立一个样式列表并将css内容写入
在这篇文章中,只讨论js的模块化方案,故,不讨论require.loadCss以及没有回调的require.loadJs。
从modJS的使用上能够看出,modJS暴露了两个全局变量define、require,如今跟随modJS源代码研究一下实现细节。 点击查看modJS的仓库地址
用define函数包裹js模块来完成模块的定义,包裹操做由打包工具fis3自动完成。
// mod.js 文件
var require, define;
(function(global) {
if (require) return; // 避免重复加载mod.js而致使已定义模块丢失
var factoryMap = {},
modulesMap = {},
loadingMap = {},
resMap = {},
pkgMap = {};
/**
* @desc 定义js模块, 用define函数包裹模块,由打包工具自动完成
* @param {String} id 模块惟一标识
* @param {Function} factory 工厂函数,接受三个参数require、exports、modules,其中exports只是modules.exports的引用
* @return void
* @example define('js/a',function(require,exports,module){ return { init: function init(){} } })
*/
define = function(id, factory) {
id = alias(id);
factoryMap[id] = factory;
var queue = loadingMap[id]; // 异步加载模块,回调函数依次执行
if (queue) {
for (var i = 0, len = queue.length; i < len; i++) {
queue[i]()
}
delete loadingMap[id]; // 从正在加载中移除
}
}
function alias(id) {
return id.replace(/\.js$/i, '');
}
})(this) // 使用函数包裹,避免污染全局变量
复制代码
好比,当咱们有一个js文件a.js,文件内容以下:
// a.js 文件
console.log('模块a');
function init() {
console.log('模块a被引用')
}
return { init: init }
// or
// exports.init=init
// or
// modules.exports={init:init}
复制代码
用打包工具进行define函数包裹后,a.js文件就变成了以下内容,此时咱们就完成了对一个标识为“js/a”的模块的包裹:
// a.js 文件
define('js/a', function(require, exports, module) {
console.log('模块a');
function init() {
console.log('模块a被引用')
}
return { init: init }
// or
// exports.init=init
// or
// modules.exports={init:init}
})
复制代码
当检测到模块被引用,打包工具会将该模块对应的srcipt标签自动嵌入HTML文档中进行预加载,加载完成后浏览器会当即执行,这样就完成了一个模块的定义。
// index.html 文件
<script src="./mod.js" type="text/javascript"></script>
<script src="./js/a.js" type="text/javascript"></script>
复制代码
在上一步操做中,完成了对模块标识为“js/a”的模块的定义,如今能够经过require(id)对已定义的模块进行引用了。 require(id)所须要作的就是初始化factory。
// mod.js 文件
var require, define;
(function(global) {
/** 此处省略部分代码 **/
/**
* @desc 同步引用已定义的js模块,若该模块未定义,则抛出 “Can not find module”错误
* @param {String} id 模块惟一标识
* @return {Object|String} 返回模块内部执行的return语句,若是模块内部没有执行return,则返回模块内部调用的 moduls.exoprts; return 优先级高于 module.exports
* @example require('js/a')
*/
require = function(id) {
id = alias(id);
var module = modulesMap[id];
// 避免重复初始化factory
if (module) {
return module.exports
}
// 初始化factory
var factory = factoryMap[id];
if (!factory) {
throw "Can not find module `" + id + "`";
}
module = modulesMap[id] = { exports: {} };
var result = typeof factory === "function" ? factory.apply(module, [require, module.exports, module]) : factory;
if (result) { // return 优先级高于 module.exports
module.exports = result;
}
return module.exports
}
function alias(id) {
return id.replace(/\.js$/i, '');
}
})(this)
复制代码
在上面的介绍中,咱们知道:经过define(id,factory)函数包裹一个模块,并使用打包工具fis3自动将该模块对应的script内嵌至HTML文档中完成模块的预加载,而后require(id)函数再引用已经预加载好的模块。 但考虑到有些模块无需在启动时载入,因此须要经过requier.async(ids,onload,onerror)进行运行时异步加载模块
那么,运行时异步加载模块须要解决那些问题呢?
对于模块内部依赖解析和模块资源定位这个两个问题,modJS是经过require.resourceMap函数解析打包工具fis3生成的rerource_map对象实现的。
好比,js目录下有三个js文件a.js、b.js、c.js,c.js引用了a.js和b.js,那么打包工具就会解析文件之间的依赖关系以及资源定位,生成一个json对象:
"pkg": {},
"res": {
"js/a": {
"url": "js/a.js",
"type": "js"
},
"js/b": {
"url": "js/b.js",
"type": "js"
},
"js/c": {
"url": "js/c.js",
"type": "js",
"deps": ["js/a", "js/b"]
}
}
复制代码
再使用require.resourceMap(obj)函数进行包裹,生成一个resource_map.js文件,内嵌至HTML文档中,浏览器加载完resource_map.js文件后,执行require.resourceMap函数就完成了模块内部依赖解析以及模块资源定位
// resource_map.js 文件
require.resourceMap({
"pkg": {},
"res": {
"js/a": {
"url": "js/a.js",
"type": "js"
},
"js/b": {
"url": "js/b.js",
"type": "js"
},
"js/c": {
"url": "js/c.js",
"type": "js",
"deps": ["js/a", "js/b"]
}
}
})
// mod.js 文件
var require, define;
(function(global) {
/** 此处省略部分代码 **/
/**
* @desc js模块依赖解析
* @param {Object} obj js模块依赖对象: { pkg: {}, res: { 'js/a': { url: 'js/a.js', type: 'js' }, 'js/b': { url: 'js/b.js', type: 'js', deps: ['js/a'] } } }
* @return void
*/
require.resourceMap = function(obj) {
var k, col;
// merge `res` & `pkg` fields
col = obj.res;
for (k in col) {
if (col.hasOwnProperty(k)) {
resMap[k] = col[k];
}
}
col = obj.pkg;
for (k in col) {
if (col.hasOwnProperty(k)) {
pkgMap[k] = col[k];
}
}
}
})(this)
// index.html
<script src="./mod.js" type="text/javascript"></script>
<script src="./resource_map.js" type="text/javascript"></script>
<script type="text/javascript">
require.async('js/c', function(mod) {
mod.init()
});
</script>
复制代码
如今,解决了模块内部依赖解析和资源定位的问题,就能够经过DOM操做动态的往HTML head标签里插入HTML script标签来异步加载模块,并在模块及模块内部依赖异步加载完成后的执行onload回调,若是异步加载失败或超时的执行onerror回调,异步加载超时时间,modJS经过require.timeout来设置,默认为5s
// mod.js 文件
var require, define;
(function(global) {
/** 此处省略部分代码 **/
var head = document.getElementsByTagName('head')[0];
/**
* @desc 异步加载js模块
* @param {String} id 模块惟一标识
* @param {Function} onload 全部的模块(包括模块内部依赖)都加载完成后执行回调函数
* @param {Function} onerror 模块加载错误或超时时执行的回调函数,超时时间经过require.timeout设置,默认5s
* @example require.async(id,onload,onerror)
* @example require.async([id1,id2,...],onload,onerror)
* @tips 先异步加载该模块,再异步加载该模块的依赖,为何这种顺序不会出现问题? 由于会等待全部的异步模块加载完毕以后才会执行onload函数
*/
require.async = function(ids, onload, onerror) {
if (typeof ids === 'string') {
ids = [ids]
}
var needMap = {},
needNum = 0;
function findDependence(depArr) {
for (var i = 0, len = depArr.length; i < len; i++) {
var dep = alias(depArr[i]);
if (dep in factoryMap) { // skip loaded
var child = resMap[dep] || resMap[dep + '.js']
if (child && 'deps' in child) { // 经过resource_map.js检查模块是否存在内部依赖,若存在,且不依赖自己,则递归内部依赖
(child.deps !== depArr) && findDependence(child.deps)
}
continue;
}
if (dep in needMap) { // skip loading
continue;
}
needMap[dep] = 1;
needNum++;
loadScript(dep, updateNeed, onerror) // 动态加载脚本。 updateNeed函数有权访问外部函数的变量(needNum,ids,onload),并只能获得这些变量的最后一个值(闭包)
var child = resMap[dep] || resMap[dep + '.js']
if (child && 'deps' in child) { // 经过resource_map.js检查模块是否存在内部依赖,若存在,且不依赖自己,则递归内部依赖
(child.deps !== depArr) && findDependence(child.deps)
}
}
}
function updateNeed() {
if (0 == needNum--) { // 等待全部的模块以及模块的内部依赖加载成功,再执行回调函数onload
var args = [];
for (var i = 0, n = ids.length; i < n; i++) {
args[i] = require(ids[i]); // 将加载完成的模块做为参数传递给onload回调函数,若是有模块为加载成功,将抛出Can not find module异常
}
typeof onload === 'function' && onload.apply(global, args) // onload函数的做用域指向全局
}
}
findDependence(ids);
updateNeed();
}
/**
* @desc 加载异步js脚本超时时间,默认5s
*/
require.timeout = 5000;
/**
* @desc 经过script标签动态加载脚本
* @param {String} id 模块惟一标识
* @param {Function} calback js模块loaded的回调函数
* @param {Function} onerror: js模块errored的回调函数
* @return void
*/
function loadScript(id, callback, onerror) {
var queue = loadingMap[id] || (loadingMap[id] = []);
queue.push(callback)
var res = resMap[id] || resMap[id + ".jd"]; // 经过resource_map.js获取模块对应的url
var pkg = res.pkg;
if (!res.url) return;
if (pkg) {
url = pkgMap[pkg].url;
} else {
url = res.url || id;
}
createScript(url, onerror && function() {
onerror(id)
});
}
function createScript(url, onerror) {
var script = document.createElement('script');
if (onerror) {
var tid = setTimeout(onerror, require.timeout); // 超时执行onerror
function onload() {
clearTimeout(tid) // loaded 清除定时器
}
if ('onload' in script) {
script.onload = onload
} else {
script.onreadystatechange = function() {
if (this.readyState === 'loaded' || this.readyState === 'complete') {
onload();
}
}
}
script.onerror = function() {
clearTimeout(tid); // errored 清除定时器
onerror()
};
}
script.src = url;
script.type = "text/javascript";
head.appendChild(script);
return script;
}
})(this);
复制代码
经过以上,能够总结出modJS实现js模块化解决方案的6个要点:
var require, define;
(function(global) {
if (require) return; // 避免重复加载mod.js而致使已定义模块丢失
var factoryMap = {},
modulesMap = {},
loadingMap = {},
resMap = {},
pkgMap = {},
head = document.getElementsByTagName('head')[0];
/**
* @desc 定义js模块, 用define函数包裹模块,由打包工具自动完成
* @param {String} id 模块惟一标识
* @param {Function} factory 工厂函数,接受三个参数require、exports、modules,其中exports只是modules.exports的引用
* @return void
* @example define('js/a',function(require,exports,module){ return { init: function init(){} } })
*/
define = function(id, factory) {
id = alias(id);
factoryMap[id] = factory;
var queue = loadingMap[id]; // 异步加载,回调函数依次执行
if (queue) {
for (var i = 0, len = queue.length; i < len; i++) {
queue[i]()
}
delete loadingMap[id]; // 从正在加载中移除
}
}
/**
* @desc 同步加载已定义的js模块,若该模块未定义,则抛出 “Can not find module”错误
* @param {String} id 模块惟一标识
* @return {Object|String} 返回模块内部执行的return语句,若是模块内部没有执行return,则返回模块内部调用的 moduls.exoprts; return 优先级高于 module.exports
* @example require('js/a')
*/
require = function(id) {
id = alias(id);
var module = modulesMap[id];
// 避免重复初始化factory
if (module) {
return module.exports
}
// 初始化factory
var factory = factoryMap[id];
if (!factory) {
throw "Can not find module `" + id + "`";
}
module = modulesMap[id] = { exports: {} };
var result = typeof factory === "function" ? factory.apply(module, [require, module.exports, module]) : factory;
if (result) { // return 优先级高于 module.exports
module.exports = result;
}
return module.exports
}
/**
* @desc 异步加载js模块
* @param {String|Array} ids 模块惟一标识
* @param {Function} onload 全部的模块(包括模块内部依赖)都加载完毕后执行回调函数
* @param {Function} onerror 模块加载错误或超时时执行的回调函数,超时时间经过require.timeout设置,默认5s
* @example require.async(id,onload,onerror)
* @example require.async([id1,id2,...],onload,onerror)
* @tips 先异步加载该模块,再异步加载该模块的依赖,为何这种顺序不会出现问题? 由于会等待全部的异步模块加载完毕以后才会执行onload函数
*/
require.async = function(ids, onload, onerror) {
if (typeof ids === 'string') {
ids = [ids]
}
var needMap = {},
needNum = 0;
function findDependence(depArr) {
for (var i = 0, len = depArr.length; i < len; i++) {
var dep = alias(depArr[i]);
if (dep in factoryMap) { // skip loaded
var child = resMap[dep] || resMap[dep + '.js']
if (child && 'deps' in child) { // 经过resource_map.js检查模块是否存在内部依赖,若存在,且不依赖自己,则递归内部依赖
(child.deps !== depArr) && findDependence(child.deps)
}
continue;
}
if (dep in needMap) { // skip loading
continue;
}
needMap[dep] = 1;
needNum++;
loadScript(dep, updateNeed, onerror) // 动态加载脚本。 updateNeed函数有权访问外部函数的变量(needNum,ids,onload),并只能获得这些变量的最后一个值(闭包)
var child = resMap[dep] || resMap[dep + '.js']
if (child && 'deps' in child) { // 经过resource_map.js检查模块是否存在内部依赖,若存在,且不依赖自己,则递归内部依赖
(child.deps !== depArr) && findDependence(child.deps)
}
}
}
function updateNeed() {
if (0 == needNum--) { // 等待全部的模块以及模块的内部依赖加载完成,再执行回调函数onload
var args = [];
for (var i = 0, n = ids.length; i < n; i++) {
args[i] = require(ids[i]); // 将加载完成的模块做为参数传递给onload回调函数,若是有模块未加载成功,将抛出Can not find module异常
}
typeof onload === 'function' && onload.apply(global, args) // onload函数的做用域指向全局
}
}
findDependence(ids);
updateNeed();
}
/**
* @desc 加载异步js脚本超时时间,默认5s
*/
require.timeout = 5000;
/**
* @desc js模块依赖解析
* @param {Object} obj js模块依赖对象: { pkg: {}, res: { 'js/a': { url: 'js/a.js', type: 'js' }, 'js/b': { url: 'js/b.js', type: 'js', deps: ['js/a'] } } }
* @return void
*/
require.resourceMap = function(obj) {
var k, col;
// merge `res` & `pkg` fields
col = obj.res;
for (k in col) {
if (col.hasOwnProperty(k)) {
resMap[k] = col[k];
}
}
col = obj.pkg;
for (k in col) {
if (col.hasOwnProperty(k)) {
pkgMap[k] = col[k];
}
}
}
/**
* @desc 经过script标签动态加载脚本
* @param {String} id 模块惟一标识
* @param {Function} calback js模块loaded的回调函数
* @param {Function} onerror: js模块errored的回调函数
* @return void
*/
function loadScript(id, callback, onerror) {
var queue = loadingMap[id] || (loadingMap[id] = []);
queue.push(callback)
var res = resMap[id] || resMap[id + ".jd"]; // 经过resource_map.js获取模块对应的url
var pkg = res.pkg;
if (!res.url) return;
if (pkg) {
url = pkgMap[pkg].url;
} else {
url = res.url || id;
}
createScript(url, onerror && function() {
onerror(id)
});
}
function createScript(url, onerror) {
var script = document.createElement('script');
if (onerror) {
var tid = setTimeout(onerror, require.timeout); // 超时执行onerror
function onload() {
clearTimeout(tid) // loaded 清除定时器
}
if ('onload' in script) {
script.onload = onload
} else {
script.onreadystatechange = function() {
if (this.readyState === 'loaded' || this.readyState === 'complete') {
onload();
}
}
}
script.onerror = function() {
clearTimeout(tid); // errored 清除定时器
onerror()
};
}
script.src = url;
script.type = "text/javascript";
head.appendChild(script);
return script;
}
function alias(id) {
return id.replace(/\.js$/i, '');
}
})(this); // 使用函数包裹,避免污染全局变量
复制代码
限于笔者水平,若是有错误或不严谨的地方,请给予指正,十分感谢。