最新文章专题视频专题问答1问答10问答100问答1000问答2000关键字专题1关键字专题50关键字专题500关键字专题1500TAG最新视频文章推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37视频文章20视频文章30视频文章40视频文章50视频文章60 视频文章70视频文章80视频文章90视频文章100视频文章120视频文章140 视频2关键字专题关键字专题tag2tag3文章专题文章专题2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章专题3
当前位置: 首页 - 科技 - 知识百科 - 正文

JavaScript模块化编程之加载器原理详解

来源:懂视网 责编:小采 时间:2020-11-27 20:24:26
文档

JavaScript模块化编程之加载器原理详解

JavaScript模块化编程之加载器原理详解:世面上有好多JavaScript的加载器,比如 sea.js, require.js, yui loader, labJs…., 加载器的使用范围是一些比较大的项目, 个人感觉如果是小项目的话可以不用, 我用过seaJS和requireJS, 在项目中用过requireJS, requireJS是符合AM
推荐度:
导读JavaScript模块化编程之加载器原理详解:世面上有好多JavaScript的加载器,比如 sea.js, require.js, yui loader, labJs…., 加载器的使用范围是一些比较大的项目, 个人感觉如果是小项目的话可以不用, 我用过seaJS和requireJS, 在项目中用过requireJS, requireJS是符合AM
世面上有好多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://www.gxlcms.com/ 直接引入页面中;

//这是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.gxlcms.com/
if ( typeof define === "function" && define.amd ) {
 define( "jquery", [], function() {
 return jQuery;
 });
};

使用方法

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

//文件所在的路径地址为:http://www.gxlcms.com/:63342/module/script/dir2/1.js
define(function() {
 return "!!!!";
});

也可以这样定义一个模块:

//这个文件的路径为http://www.gxlcms.com/: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.gxlcms.com/
 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.gxlcms.com/:4:1
 *firefox17:
 *@http://www.gxlcms.com/:4
 *opera12:http://www.gxlcms.com/
 *@http://www.gxlcms.com/:4
 *IE10:
 * at Global code (http://www.gxlcms.com/: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的加载器,比如 sea.js, require.js, yui loader, labJs…., 加载器的使用范围是一些比较大的项目, 个人感觉如果是小项目的话可以不用, 我用过seaJS和requireJS, 在项目中用过requireJS, requireJS是符合AM
推荐度:
标签: 加载 原理 js
  • 热门焦点

最新推荐

猜你喜欢

热门推荐

专题
Top