JavaScript 模块化编程之加载器原理详解
AMD__和__CMD
AMD规范是依赖前置, CMD规范是依赖后置, AMD规范的加载器会把所有的JS中的依赖前置执行。 CMD是懒加载, 如果JS需要这个模块就加载, 否则就不加载, 导致的问题是符合AMD规范的加载器(requireJS), 可能第一次加载的时间会比较久, 因为他把所有依赖的JS全部一次性下载下来;
常识,jQuery是支持AMD规范,并不支持CMD规范,也就是说, 如果引入的是seaJS,想要使用jQuery,要用alias配置, 或者直接把 http://www.php.cn/ 直接引入页面中;
//这是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 // http://www.php.cn/ if ( typeof define === "function" && define.amd ) { define( "jquery", [], function() { return jQuery; }); };
使用方法
比如我们可以这样定义一个模块:
//文件所在的路径地址为:http://www.php.cn/:63342/module/script/dir2/1.js define(function() { return "!!!!"; });
也可以这样定义一个模块:
//这个文件的路径为http://www.php.cn/: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属性; // 参考 http://www.php.cn/ 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://www.php.cn/:4:1 *firefox17: *@http://www.php.cn/:4 *opera12:http://www.php.cn/ *@http://www.php.cn/:4 *IE10: * at Global code (http://www.php.cn/: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 模块化编程之加载器原理详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!