你的位置:首页 > Java教程

[Java教程]JS模块化编程之加载器原理

  世面上有好多JavaScript的加载器,比如 sea.js, require.js, yui loader, labJs...., 加载器的使用范围是一些比较大的项目, 个人感觉如果是小项目的话可以不用,  我用过seaJSrequireJS, 在项目中用过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-anonif ( typeof define === "function" && define.amd ) {  define( "jquery", [], function() {    return jQuery;  });};

   

使用方法

  比如我们可以这样定义一个模块:

//文件所在的路径地址为:http://localhost:63342/module/script/dir2/1.jsdefine(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:         [email protected]://113.93.50.63/query.js:4         *opera12:http://www.oldapps.com/opera.php?system=Windows_XP         [email protected]://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() //取得最后一行,[email protected]        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>

View Code

 

  一个例子

   写一个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学习。

参考:

  JS魔法堂:获取当前脚本文件的绝对路径

  JavaSript模块规范 - AMD规范与CMD规范介绍

  【模块化编程】理解requireJS-实现一个简单的模块加载器

 

作者: NONO
出处:http://www.cnblogs.com/diligenceday/
QQ:287101329