JavaScript 模組化程式設計之載入器原理
世面上有好多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>
例子的原始碼下載地址: 下載
相關文章
- javascript 模組化程式設計JavaScript程式設計
- JavaScript模組化程式設計規範JavaScript程式設計
- JavaScript 模組化程式設計 - Module PatternJavaScript程式設計
- javascript模組化以及載入打包JavaScript
- JavaScript 模組化程式設計(一):模組的寫法JavaScript程式設計
- Web前端進階之JavaScript模組化程式設計知識Web前端JavaScript程式設計
- 【模組化程式設計】理解requireJS-實現一個簡單的模組載入器程式設計UIJS
- 簡述JavaScript模組化程式設計(二)JavaScript程式設計
- JavaScript 模組化程式設計簡單介紹JavaScript程式設計
- JavaScript 模組化程式設計(二):AMD規範JavaScript程式設計
- Javascript模組化程式設計(二):AMD規範JavaScript程式設計
- js模組化之自定義模組(頁面模組化載入)JS
- 前端模組化之迴圈載入前端
- JavaScript 模組載入特性JavaScript
- JavaScript模組化原理淺析JavaScript
- 一個簡單的JavaScript模組載入器JavaScript
- UEFI載入程式 & 驅動模組化
- Javascript模組化程式設計(三):require.js的用法JavaScript程式設計UIJS
- 模組載入器
- JS模組化程式設計JS程式設計
- JavaScript ES 模組:現代化前端程式設計必備技能JavaScript前端程式設計
- 爬蟲逆向基礎,理解 JavaScript 模組化程式設計 webpack爬蟲JavaScript程式設計Web
- 《程式設計時間簡史系列》JavaScript 模組化的歷史程式程式設計JavaScript
- JavaScript面試系列:JavaScript設計模式之橋接模式和懶載入JavaScript面試設計模式橋接
- Webpack模組載入器Web
- JavaScript 模組的迴圈載入JavaScript
- 深入理解模組化程式設計程式設計
- Javascript超程式設計之AnnotationJavaScript程式設計
- Shell程式設計入門:Linux直譯器原理(轉)程式設計Linux
- 《JavaScript框架設計(第2版)》之語言模組JavaScript框架
- 瀏覽器載入 CommonJS 模組的原理與實現瀏覽器JS
- Javascript設計模式之迭代器模式JavaScript設計模式
- webpack模組非同步載入原理解析Web非同步
- Layui 原始碼淺讀(模組載入原理)UI原始碼
- Spring-boot模組化程式設計Springboot程式設計
- RequireJS 模組化程式設計詳解UIJS程式設計
- Nim語言的模組化程式設計程式設計
- JavaScript 非同步程式設計入門JavaScript非同步程式設計