世面上有好多JavaScript的加載器,比如 sea.js, require.js, yui loader, labJs...., 加載器的使用范圍是一些比較大的項目, 個人感覺如果是小項目的話可以不用, 我用過seaJS和requireJS, 在項目中用過requireJS, requireJS是符合AMD,全稱是(Asynchronous Module Definition)即異步模塊加載機制 , seaJS是符合CMD規范的加載器。
AMD__和__CMD
AMD規范是依賴前置, CMD規范是依賴后置, AMD規范的加載器會把所有的JS中的依賴前置執行 。 CMD是懶加載, 如果JS需要這個模塊就加載, 否則就不加載, 導致的問題是符合AMD規范的加載器(requireJS), 可能第一次加載的時間會比較久, 因為他把所有依賴的JS全部一次性下載下來;
常識,jQuery是支持AMD規范,並不支持CMD規范,也就是說, 如果引入的是seaJS,想要使用jQuery,要用alias配置, 或者直接把 http://cdn.bootcss.com/jquery/2.1.4/jquery.js 直接引入頁面中;
//這是jQuery源碼的最后幾行, jQuery到了1.7才支持模塊化; // Register as a named AMD module, since jQuery can be concatenated with other // files that may use define, but not via a proper concatenation script that // understands anonymous AMD modules. A named AMD is safest and most robust // way to register. Lowercase jquery is used because AMD module names are // derived from file names, and jQuery is normally delivered in a lowercase // file name. Do this after creating the global so that if an AMD module wants // to call noConflict to hide this version of jQuery, it will work. // Note that for maximum portability, libraries that are not jQuery should // declare themselves as anonymous modules, and avoid setting a global if an // AMD loader is present. jQuery is a special case. For more information, see // https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon if ( typeof define === "function" && define.amd ) { define( "jquery", [], function() { return jQuery; }); };
使用方法
比如我們可以這樣定義一個模塊:
//文件所在的路徑地址為:http://localhost:63342/module/script/dir2/1.js define(function() { return "!!!!"; });
也可以這樣定義一個模塊:
//這個文件的路徑為http://localhost:63342/module/main.js ,而且有一個依賴, 加載器會自動去加載這個依賴, 當依賴加載完畢以后, 會把這個依賴(就是script/dir2/1.js)執行的返回值作為這個函數的參數傳進去; require(["script/dir2/1.js"], function(module1) { console.log(module1); });
//實際上會打印出 "!!!!"
一般來說,一個模塊只能寫一個define函數, define函數的傳參主要有兩種方式:
1:正常上可以是一個函數;
2:可以是一個數組類型依賴的列表, 和一個函數;
如果一個模塊寫了多個define會導致模塊失靈, 先定義的模塊被后定義的模塊給覆蓋了 ( 當然了, 一般我們不那樣玩);
一個模塊內可以寫多個require, 我們可以直接理解require為匿名的define模塊, 一個define模塊內可以有多個require, 而且require過的模塊會被緩存起來, 這個緩存的變量一般是在閉包內, 而且名字多數叫modules什么的.....;
我們通過加載器開發實現的模塊化開發要遵守一種規范, 規范了一個模塊為一個JS,那么我們就可以新建幾個目錄為conroller,view, model, 也是為了后期更好的維護和解耦:

實現一個自己的加載器
使用的方式:
//這個模塊依賴的四個模塊,加載器會分別去加載這四個模塊;
define(["依賴0","依賴1","依賴2","依賴3"], function(依賴0,依賴1,依賴2,依賴3){
});
//返回一個空對象
define(function(){
return {};
});
//直接把require當作是define來用就好了;
require(["依賴0","依賴1","依賴2","依賴3"], function(依賴0,依賴1,依賴2,依賴3) {
//執行依賴0;
依賴0(依賴1,依賴2,依賴3);
});
//這個加載器define函數和require函數的區別是,define我們可以傳個name作為第一參數, 這個參數就是模塊的名字😴😠, 好吧, 不管這些了.....;
以下為加載器的結構,因為代碼量已經很少了, 所以每一函數都是必須的, 為了不影響全局, 把代碼放在匿名自執行函數內部:
(function() { 定義一個局部的difine; var define; //我偷偷加了個全局變量,好調試啊; window.modules = { }; //通過一個名字獲取絕對路徑比如傳"xx.js"會變成"http://www.mm.com/"+ baseUrl + "xx.html"; var getUrl = function(src) {}; //動態加載js的模塊; var loadScript = function(src) {}; //獲取根路徑的方法, 一般來說我們可以通過config.baseUrl配置這個路徑; var getBasePath = function() {}; //獲取當前正在加載的script標簽DOM節點; var getCurrentNode = function() {}; //獲取當前script標簽的絕對src地址; var getCurrentPath = function() {}; //加載define或者require中的依賴, 封裝了loadScript方法; var loadDpt = function(module) {}; //這個是主要模塊, 完成了加載依賴, 檢測依賴等比較重要的邏輯 var checkDps = function() {}; 定義了define這個方法 define = function(deps, fn, name) {}; window.define = define; //require是封裝了define的方法, 就是多傳了一個參數而已; window.require = function() { //如果是require的話那么模塊的名字就是一個不重復的名字,避免和define重名; window.define.apply([], Array.prototype.slice.call(arguments).concat( "module|"+setTimeout(function() {},0) )); }; });
加載器源碼實現(兼容,chrome, FF, IE6 ==>> IE11), IE11沒有了readyState屬性, 也沒有currentScript屬性,坑爹啊, 無法獲取當前正在執行的JS路徑, 所以要用hack;
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <script> (function() { var define; window.modules = { }; var getUrl = function(src) { var scriptSrc = ""; //判斷URL是否是 // ./或者 // /或者 // 直接是以字符串開頭 // 或者是以http://開頭; if( src.indexOf("/") === 0 || src.indexOf("./") === 0 ) { scriptSrc = require.config.base + src.replace(/^\//,"").replace(/^\.\//,""); }else if( src.indexOf("http:") === 0 ) { scriptSrc = src; }else if( src.match(/^[a-zA-Z1-9]/) ){ scriptSrc = require.config.base + src; }else if(true) { alert("src錯誤!"); }; if (scriptSrc.lastIndexOf(".js") === -1) { scriptSrc += ".js"; }; return scriptSrc; }; var loadScript = function(src) { var scriptSrc = getUrl(src); var sc = document.createElement("script"); var head = document.getElementsByTagName("head")[0]; sc.src = scriptSrc; sc.onload = function() { console.log("script tag is load, the url is : " + src); }; head.appendChild( sc ); }; var getBasePath = function() { var src = getCurrentPath(); var index = src.lastIndexOf("/"); return src.substring(0,index+1); }; var getCurrentNode = function() { if(document.currentScript) return document.currentScript; var arrScript = document.getElementsByTagName("script"); var len = arrScript.length; for(var i= 0; i<len; i++) { if(arrScript[i].readyState === "interactive") { return arrScript[i]; }; }; //IE11的特殊處理; var path = getCurrentPath(); for(var i= 0; i<len; i++) { if(path.indexOf(arrScript[i].src)!==-1) { return arrScript[i]; }; }; throw new Error("getCurrentNode error"); }; var getCurrentPath = function() { var repStr = function(str) { return (str || "").replace(/[\&\?]{1}[\w\W]+/g,"") || ""; }; if(document.currentScript) return repStr(document.currentScript.src); //IE11沒有了readyState屬性, 也沒有currentScript屬性; // 參考 https://github.com/samyk/jiagra/blob/master/jiagra.js var stack try { a.b.c() //強制報錯,以便捕獲e.stack } catch (e) { //safari的錯誤對象只有line,sourceId,sourceURL stack = e.stack if (!stack && window.opera) { //opera 9沒有e.stack,但有e.Backtrace,但不能直接取得,需要對e對象轉字符串進行抽取 stack = (String(e).match(/of linked script \S+/g) || []).join(" ") } } if (stack) { /**e.stack最后一行在所有支持的瀏覽器大致如下: *chrome23: * at http://113.93.50.63/data.js:4:1 *firefox17: *@http://113.93.50.63/query.js:4 *opera12:http://www.oldapps.com/opera.php?system=Windows_XP *@http://113.93.50.63/data.js:4 *IE10: * at Global code (http://113.93.50.63/data.js:4:1) * //firefox4+ 可以用document.currentScript */ stack = stack.split(/[@ ]/g).pop() //取得最后一行,最后一個空格或@之后的部分 stack = stack[0] === "(" ? stack.slice(1, -1) : stack.replace(/\s/, "") //去掉換行符 return stack.replace(/(:\d+)?:\d+$/i, "") //去掉行號與或許存在的出錯字符起始位置 }; //實在不行了就走這里; var node = getCurrentNode(); //IE>=8的直接通過src可以獲取,IE67要通過getAttriubte獲取src; return repStr(document.querySelector ? node.src : node.getAttribute("src", 4)) || ""; throw new Error("getCurrentPath error!"); }; var loadDpt = function(module) { var dp = ""; for(var p =0; p<module.dps.length; p++) { //獲取絕對的地址; var dp = getUrl(module.dps[p]); //如果依賴沒有加載就直接加載; if( !modules[dp] ) { loadScript(dp); }; }; }; //主要的模塊, 檢測所有未加載的模塊中未完成了的依賴是否加載完畢,如果加載完畢就加載模塊, 如果加載過的話,而且所有依賴的模塊加載完畢就執行該模塊 //而且此模塊的exports為該模塊的執行結果; var checkDps = function() { for(var key in modules ) { //初始化該模塊需要的參數; var params = []; var module = modules[key]; //加載完畢就什么都不做; if( module.state === "complete" ) { continue; }; if( module.state === "initial" ) { //如果依賴沒有加載就加載依賴並且modules沒有該module就加載這個模塊; loadDpt(module); module.state = "loading"; }; if( module.state === "loading") { //如果這個依賴加載完畢 for(var p =0; p<module.dps.length; p++) { //獲取絕對的地址; var dp = getUrl(module.dps[p]); //如果依賴加載完成了, 而且狀態為complete;; if( modules[dp] && modules[dp].state === "complete") { params.push( modules[dp].exports ); }; }; //如果依賴全部加載完畢,就執行; if( module.dps.length === params.length ) { if(typeof module.exports === "function"){ module.exports = module.exports.apply(modules,params); module.state = "complete"; //每一次有一個模塊加載完畢就重新檢測modules,看看是否有未加載完畢的模塊需要加載; checkDps(); }; }; }; }; }; //[],fn; fn define = function(deps, fn, name) { if(typeof deps === "function") { fn = deps; deps = [];//我們要把數組清空; }; if( typeof deps !== "object" && typeof fn !== "function") { alert("參數錯誤") }; var src = getCurrentPath(); //沒有依賴, 沒有加載該模塊就新建一個該模塊; if( deps.length===0 ) { modules[ src ] = { name : name || src, src : src, dps : [], exports : (typeof fn === "function")&&fn(), state : "complete" }; return checkDps(); }else{ modules[ src ] = { name : name || src, src : src, dps : deps, exports : fn, state : "initial" }; return checkDps(); } }; window.define = define; window.require = function() { //如果是require的話那么模塊的名字就是一個不重復的名字,避免和define重名; window.define.apply([], Array.prototype.slice.call(arguments).concat( "module|"+setTimeout(function() {},0) )); }; require.config = { base : getBasePath() }; require.loadScript = loadScript; var loadDefaultJS = getCurrentNode().getAttribute("data-main"); loadDefaultJS && loadScript(loadDefaultJS); })(); </script> </head> <body> </body> </html>
從葉大大那邊偷的一個加載器, 這個加載器有點像jQuery中延遲對象($.Deferred)有關的方法when($.when)的實現;
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <script> (function () { //存儲已經加載好的模塊 var moduleCache = {}; var define = function (deps, callback) { var params = []; var depCount = 0; var i, len, isEmpty = false, modName; //獲取當前正在執行的js代碼段,這個在onLoad事件之前執行 modName = document.currentScript && document.currentScript.id || 'REQUIRE_MAIN'; //簡單實現,這里未做參數檢查,只考慮數組的情況 if (deps.length) { for (i = 0, len = deps.length; i < len; i++) { (function (i) { //依賴加一 depCount++; //這塊回調很關鍵 loadMod(deps[i], function (param) { params[i] = param; depCount--; if (depCount == 0) { saveModule(modName, params, callback); } }); })(i); } } else { isEmpty = true; } if (isEmpty) { setTimeout(function () { saveModule(modName, null, callback); }, 0); } }; //考慮最簡單邏輯即可 var _getPathUrl = function (modName) { var url = modName; //不嚴謹 if (url.indexOf('.js') == -1) url = url + '.js'; return url; }; //模塊加載 var loadMod = function (modName, callback) { var url = _getPathUrl(modName), fs, mod; //如果該模塊已經被加載 if (moduleCache[modName]) { mod = moduleCache[modName]; if (mod.status == 'loaded') { setTimeout(callback(this.params), 0); } else { //如果未到加載狀態直接往onLoad插入值,在依賴項加載好后會解除依賴 mod.onload.push(callback); } } else { /* 這里重點說一下Module對象 status代表模塊狀態 onLoad事實上對應requireJS的事件回調,該模塊被引用多少次變化執行多少次回調,通知被依賴項解除依賴 */ mod = moduleCache[modName] = { modName: modName, status: 'loading', export: null, onload: [callback] }; _script = document.createElement('script'); _script.id = modName; _script.type = 'text/javascript'; _script.charset = 'utf-8'; _script.async = true; _script.src = url; //這段代碼在這個場景中意義不大,注釋了 // _script.onload = function (e) {}; fs = document.getElementsByTagName('script')[0]; fs.parentNode.insertBefore(_script, fs); } }; var saveModule = function (modName, params, callback) { var mod, fn; if (moduleCache.hasOwnProperty(modName)) { mod = moduleCache[modName]; mod.status = 'loaded'; //輸出項 mod.export = callback ? callback(params) : null; //解除父類依賴,這里事實上使用事件監聽較好 while (fn = mod.onload.shift()) { fn(mod.export); } } else { callback && callback.apply(window, params); } }; window.require = define; window.define = define; })(); </script> </head> <body> </body> </html>
一個例子
寫一個MVC的小例子,代碼簡單, 高手無視, 目錄結構如下:

我們把所有的事件放到了controller/mainController.js里面,
define(["model/data","view/view0"],function(data, view) { var init = function() { var body = document.getElementsByTagName("body")[0]; var aBtn = document.getElementsByTagName("button"); for(var i=0; i< aBtn.length; i++) { aBtn[i].onclick = (function(i) { return function() { body.appendChild( view.getView(data[i]) ); }; })(i); }; }; return { init : init }; });
把所有的數據放到了model/data.js里面;
define(function() { return [ {name : "qihao"}, {name : "nono"}, {name : "hehe"}, {name : "gege"} ]; })
視圖的JS放到了view的目錄下,view0.js主要負責生成HTML字符串或者DOM節點;
define(function() { return { getView : function(data) { var frag = document.createDocumentFragment(); frag.appendChild( document.createTextNode( data.name + " ") ); return frag; } } });
入口是app.js,他和load.html是同級目錄:
require(["controller/mainController"],function( controller ) { controller.init(); });
load.html這個是主界面:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title></head> <body> <button>0</button> <button>1</button> <button>2</button> <button>3</button> <script src="require.js" data-main="app.js"></script> </body> </html>
例子的源碼下載地址: 下載
下周開始復習jQuery1.8版本源碼, 然后下下周准備寫些書籍的讀后感, 包括js設計模式,js忍者,js眾妙之門,reactjs學習。
參考:
【模塊化編程】理解requireJS-實現一個簡單的模塊加載器
作者: NONO
出處:http://www.cnblogs.com/diligenceday/
QQ:287101329
