学着写一个异步模块加载器

阿凡达2018-07-09 13:56
一年前,刚来网易实习的时候接触了NEJ,那是第一次接触模块化开发,感觉想出这个点子的人简直是天才,同时也对于这种框架的实现非常好奇,惭愧的是,那时甚至连jQuery的原理都不知道。
随着这一年对于JS面向对象的理解有所加深,看着《JavaScript设计模式》就跟着自己动手码码代码,所以这是一篇读书笔记,并不是发明创造,并且这个加载器是比较简陋的,很有改进空间。
模块的长相
模块采用的是匿名模块,它的js绝对路径作为它的惟一标识:
define([
    '{lib}dom',
    '{pro}extend'    
], function(dom, extend) {
    //TODO
})
异步加载的思路
从上面我们可以看出,模块是由define函数来定义,传入参数为:依赖列表和回调函数,为了实现依赖注入,要等到依赖列表的所有js加载完后再来执行回调函数。
所以第一步,我们循环遍历依赖列表,然后依次加载列表的模块,可想而知,在循环遍历加载模块的代码的结构应该是下面这样子的:
//modules = ['lib/dom.js', 'js/extend.js']
var modCount = modules.length;
var params = [];  //保存依赖列表的对象
for (var i = 0, len = modules.length; i < len; i++) {
    (function(i){
        var url = modules[i];
        loadModule(url, function(module) {
            modCount--;
            params[i] = module;
            if (modCount == 0) {
                defineModule(uid, params, callback);   //uid为该模块绝对路径,callback为传入的回调函数
            }
        })
    })(i)
}
上面的代码只是部分代码,但是我们可以看到思路就是循环加载模块,同时传入一个回调,加载完成后触发回调,回调函数里会将modCount(模块个数)减1,如果modCount变为0,那么说明就全部模块都加载完成了,就执行defineModule函数,同时传入全部的依赖对象。
异步加载触发回调
要触发回调,首先要知道什么时候js脚本什么时候加载完成。我们创建一个script标签,append进body,这样就可以加载js脚本,那么什么时候脚本加载完成呢?
有的人可能马上就想到了,当js代码开始执行的时候就说明这个脚本加载完了。注意,只是这个脚本,不要忘记在这个脚本当中,我们可能还依赖了其他模块,这样我们还要等待这个依赖模块加载完它所拥有的依赖模块列表后执行其回调函数才算这个模块加载完成。
所以这样子我们可以知道最终的加载完成的标志就是执行defineModule函数,所以在loadModule函数中,我们需要将加载回调函数进行缓存,等待后面加载完成后执行。
loadModule函数
//moduleCache = {} 是定义在全局的一个模块缓存对象
    
function loadModule(uid, callback) {
    var _module;
    if (moduleCache[uid]) {
        _module = moduleCache[uid];
        if (_module.status == 'loaded') {
	    setTimeout(callback(_module.exports), 0);
	} else {
	    _module.onload.push(callback);
	}
    } else {
	moduleCache[uid] = {
	    uid: uid,
	    status: 'loading',
	    exports: null,
            onload: [callback]
	};
	loadScript(uid);
    }
}
	
function loadScript(url) {
    var _script = document.createElement('script');
    _script.charset = 'utf-8';
    _script.async = true;
    _script.src = url;
    document.body.appendChild(_script);
}
上面代码的思路是加载模块的时候,先在缓存对象中寻找看看有没有存在的模块:
  1. 存在,那么就看是已经加载完了还是在加载当中,如果加载中,那么就在其回调列表push一个新的回调。
  2. 不存在,那么就往缓存中添加一个新的模块,exports保存这个模块的对象,onload保存这个模块加载完成后的回调函数执行列表。然后添加script标签。
defineModule函数
到这里,我们可以感觉到快要写完了,但是我们仍然没有执行加载模块后的回调函数,上面也交代了,模块加载完成后总会执行defineModule函数,所以在这里执行回调,上代码:
function defineModule(uid, params, callback) {
    if (moduleCache[uid]) {
        var _module = moduleCache[uid];
	_module.status = 'loaded';
	_module.exports = callback ? callback.apply(_module, params) : null;
	while (fn = _module.onload.shift()) {
		fn(_module.exports);
	}
    } else {
        moduleCache[uid] = {
	    uid: uid,
	    status: 'loaded',
	    onload: [],
	    exports: callback && callback.apply(null, params)
        }
    }
}
可以看到,定义模块时我们判断是否存在,如果存在,说明这个模块是被依赖的,所以就执行onload里缓存的回调函数。
添添补补
上面就把功能实现了,但是还是有不少问题的,比如依赖列表的js路径问题,uid怎么获取,还有可能需要加载html文件等等,但是这些都是一些小问题,整体模块加载器已经完成,剩下的就是修修补补,下面附上我目前的define.js文件代码:
(function(win, doc){

	var moduleCache = {};

	var t = /(\S+)define\.js(?:\?pro=(\S+))?/.exec(getCurrentUrl()),
		lib = t[1],
		pro = t[2] || '/',
		dir = win.location.href;
	var tReg = /^\.\/|^\//;

	while (tReg.test(pro)) {
		pro = pro.replace(tReg, '')
	}

	var backCount = 0;
	tReg = /^\.\.\//;
	while (tReg.test(pro)) {
		backCount++;
		pro = pro.replace(tReg, '')
	}

	pro = backUrl(lib, backCount) + pro;


	var tplReg = /\.html$/;


	function getCurrentUrl(){
		return document.currentScript.src;
	}

	function backUrl(url, count) {
		for (var i = 0; i < count; i++) {
			url = url.replace(/[^/]+\/?$/, '');
		}
		return url;
	}

	function fixUrl(url) {
		if (tplReg.test(url)) {
			if (/^\{lib\}/.test(url)){
				return url.replace(/^\{lib\}/, lib);
			} else if (/^\{pro\}/.test(url)) {
				return url.replace(/^\{pro\}/, pro);
			} else {
				return url;
			}
		}
		return url.replace(/^\{lib\}/, lib).replace(/^\{pro\}/, pro).replace(/\.js$/g, '') + '.js';
	}

	function loadScript(url) {
		var _script = document.createElement('script');
		_script.charset = 'utf-8';
		_script.async = true;
		_script.src = fixUrl(url);
		document.body.appendChild(_script);
	}

	function defineModule(uuid, mParams, callback) {
		if (moduleCache[uuid]) {
			var _module = moduleCache[uuid];
			_module.status = 'loaded';
			_module.exports = callback ? callback.apply(_module, mParams) : null;
			while (fn = _module.onload.shift()) {
				fn(_module.exports);
			}
		} else {
			moduleCache[uuid] = {
				uuid: uuid,
				status: 'loaded',
				exports: callback && callback.apply(null, mParams),
				onload: []
			}
		}
	}


	function loadModule(uuid, callback) {
		var _module;
		if (moduleCache[uuid]) {
			_module = moduleCache[uuid];
			if (_module.status == 'loaded') {
				setTimeout(callback(_module.exports), 0);
			} else {
				_module.onload.push(callback);
			}
		} else {
			moduleCache[uuid] = {
				uuid: uuid,
				status: 'loading',
				exports: null,
				onload: [callback]
			};
			loadScript(uuid);
		}
	}


	var define = function(modules, callback) {
		
		modules = Array.isArray(modules) ? modules : [];

		for (var i = 0, len = modules.length; i < len; i++) {

			modules[i] = fixUrl(modules[i]);
		}

		var uuid = getCurrentUrl(),
			mlen = modules.length,
			mParams = [],
			i = 0,
			loadCount = 0;

		if (mlen) {
			while (i < mlen) {
				loadCount++;
				(function(i){
					if (tplReg.test(modules[i])) {
						loadText(modules[i], function(_json){
							
							var tpl = '';

							if (_json.code == 200) {
								tpl = _json.result;
							}
							loadCount--;
							mParams[i] = tpl;
							if (loadCount == 0) {
								defineModule(uuid, mParams, callback);
							}
						})
					} else {

						loadModule(modules[i], function(module) {
							loadCount--;
							mParams[i] = module;
							if (loadCount == 0) {
								setModule(uuid, mParams, callback);
							}

						});
					}

				})(i);
				i++;
			}
		} else {
			defineModule(uuid, [], callback)
		}


	}


	function loadText(url, callback) {
		var xhr = new XMLHttpRequest();
		xhr.open("get", url, true);
		xhr.send(null);
		xhr.onreadystatechange = function() {
			if (xhr.readyState == 4) {
				if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
					var code = 200;
				} else {
					code = xhr.status;
				}
				callback({
					code: code,
					result: xhr.responseText
				})
			}
		}
	}


	loadScript(fixUrl('{lib}router'));

	win.define = define;

	win.gObj = {
		loadScript: loadScript,
		loadText: loadText,
		lib: lib,
		pro: pro,
		fixUrl: fixUrl
	}

})(window, document)
这个加载器目前我知道的问题有:
  1. 无法处理循环依赖的问题,也就是a依赖b,b再依赖a,并不会报错。
  2. 获取js路径函数没有做兼容处理,在IE上并不能这么获取
  3. 代码写得比较糙,至少在路径上处理可以做优化

本文来自网易实践者社区,经作者康东扬授权发布。