天天看點

學着寫一個異步子產品加載器

一年前,剛來網易實習的時候接觸了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. 代碼寫得比較糙,至少在路徑上處理可以做優化

本文來自網易雲社群,經作者康東揚授權釋出。

原文位址:學着寫一個異步子產品加載器

更多網易研發、産品、營運經驗分享請通路網易雲社群。 

繼續閱讀