一年前,剛來網易實習的時候接觸了NEJ,那是第一次接觸模塊化開發,感覺想出這個idea的人簡直是天才,同時也對於這種框架的實現非常好奇,慚愧的是,那時甚至連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);
}
上面代碼的思路是加載模塊的時候,先在緩存對象中尋找看看有沒有存在的模塊。
- 存在,那么就看是已經加載完了還是在加載當中,如果加載中,那么就在其回調列表push一個新的回調。
- 不存在,那么就往緩存中添加一個新的模塊,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) {
defineModule(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)
這個加載器目前我知道的問題有:
-
無法處理循環依賴的問題,也就是a依賴b,b再依賴a,並不會報錯。
-
獲取js路徑函數沒有做兼容處理,在IE上並不能這么獲取
-
代碼寫得比較糙,至少在路徑上處理可以做優化