任何一门语言在大规模应用阶段,必然要经历拆分模块的过程。便于维护与团队协作,与java走的最近的dojo率先引入加载器,早期的加载器都是同步的,使用document.write与同步Ajax请求实现。后来dojo开始以JSONP的方法设计它的每个模块结构。以script节点为主体加载它的模块。这个就是目前主流的加载器方式。

不得不提的是,dojo的加载器与AMD规范的发明者都是james Burke,dojo加载器独立出来就是著名的require。本章将深入的理解加载器的原理。

1.AMD规范

AMD是"Asynchronous Module Definition"的缩写,意思是“异步模块定义”。重点有两个。

  • 异步:有效的避免了采用同步加载导致页面假死的情况。
  • 模块定义:每个模块必须按照一定的格式编写。主要的接口有两个,define与require。define是模块开发者关注的方法,require是模块使用者所关注的方法。

define的参数的情况是define(id?,deps,factory)。第一个为模块ID,第二个为依赖列表,第三个是工厂方法。前两个都是可选,如果不定义ID,则是匿名模块,加载器能应用一些“魔术”能让它辨识自己叫什么,通常情况,模块id约等于模块在过程中的路径(放在线上,表现为url)。在开发过程中,很多情况未确定,一些javascript文件会移来移去的,因此,匿名模块就大发所长。deps和factory有个约定,deps有多少个元素,factory就有多少个传参,位置一一对应。传参为其它模块的返回值。

    define("xxx",["aaa","bbb"], function (aaa,bbb){
//code
});

通常情况下,define中还有一个amd对象,里面存储着模块的相关信息。

require的参数的情况是 require(deps,callback) ,第一个为依赖列表,第二个为回调。deps有多少个元素,callback就有多少个传参,情况与define方法一致。因此在内部,define方法会调用require来加载依赖模块,一直这样递归下去。

require(["aaa","bbb"],function(aaa,bbb){
//code
})

接口就是这么简单,但require本身还包含许多特性,比如使用“!”来引入插件机制,通过requirejs.config进行各种配置。模块只是整合的一部分,你要拆的开,也要合的来,因此合并脚本的地位在加载器中非常重要,但前端javascript没有这功能,requirejs利用node.js写了一个r.js帮你进行合并。

2.加载器所在的路径探知

要加载一个模块,我们需要一个url作为加载地址,一个script作为加载媒介。但用户在require时都用id,因此,我们需要一个将id转换为url的方法。思路很简单,约定为:

    basePath + 模块id + ".js"

由于浏览器自上而下的分析DOM,当浏览器在解析我们的javascript文件(这个javascript文件是指加载器)时,它就肯定DOM树中最后加入script标签,因此,我们下面的这个方法。

        function getBasePath(){
var nodes = document.getElementsByTagName("script");
var node = nodes[nodes.length - 1];
var src = document.querySelector ? node.src : node.getAttribute("src",4);
return src;

上面的这个办法满足99%的需求,但是我们不得不动态加载我们的加载器呢?在旧的版本的IE下很多常规的方法都会失效,除了API差异性,它本身还有很多bug,我们很难指出是什么,总之要解决,如下面的这个javascript判断。

    document.write('<script src="avalon.js"> <\/script>');
document.write('<script src="mass.js"> <\/script>');
document.write('<script src="jQuery.js"> <\/script>');

mass.js为我们的加载器,里面执行getBasePath方法,预期得到http://1.1.1/mass.js,但是IE7确返回http://1.1.1/jQuery.js

这时就需要readyChange属性,微软在document、image、xhr、script等东西都拥有了这个属性。用来查看加载情况

    function getBasePath() {
var nodes = document.getElementsByTagName("script");
if (window.VBArray){ //如果是IE
for (var i = 0 ; nodes; node = nodes[i++]; ) {
if (node.readyState === "interactive") {
break;
}
}
} else {
node = nodes[nodes.length - 1];
}
var src = document.querySelector ? node.src : node.getAttribute("src",4);
return src;
}

这样就搞定了,访问DOM比一般javascript代码消耗高许多。这样,我们就可以使用Error对象。

    function getBasePath() {
try {
a.b.c()
} catch (e) {
if (e.fileName) { //FF
return e.fileName;
} else if ( e.sourceURL ){ //safari
return e.sourceURL;
}
} var nodes = document.getElementsByTagName("script");
if (window.VBArray){//倒序查找的性能更高
for (var i = nodes.length; node ; node = nodes[--i];) {
if ( node.readyState === "interactive") {
break;
}
};
} else {
node = nodes[nodes.length - 1];
}
var src = document.querySelector ? node.src : node.getAttribute("src",4);
return src;
}

在实际使用中,我们为了防止缓存,这个后面可能带版本号,时间戳什么的,也要去掉

    url = url.replace(/[?#].*/, "").slice(0, url.lastIndexOf("/") + 1);

3.require方法

require方法的作用是当前依赖列表都加载完毕,执行用户回调。因此,这里有个加载过程,整个加载过程细分以下几步:

(1) 取到依赖列表的第一个id ,转换为url ,无论是通过basePath + ID + ".js"还是通过映射方式直接得到。

(2) 检测此模块有没有加载过,或正在被加载。因此有一个对象保持所有模块的加载情况,如果有模块从来没有加载过,就进入加载流程。

(3) 创建script节点,绑定onerror,onload,onredyChange等事件判定加载成功与否,然后添加src并插入DOM树。开始加载url

(4) 将模块的url,依赖列表等构建成一个对象,放到检测队列中,在上面事件触发时进行检测。

模块id的转换规则:http://wiki.commonjs.org/wiki/Modules/1.1.1

除了basePath,我们通常还用到映射,就是用户事前用一个方法,把id和完整的url对应好,这样就直接拿。此外,AMD规范还有shim技术。shim机制的目的是让不符合AMD规范的js文件也能无缝切入我们的加载系统。

普通别名机制:

    require.config({
alias:{
'lang' : 'http://xxx.com/lang.js',
'css' : 'http://bbb.com/css.js'
}
})

jQuery或其它插件,我们需要shim机制

    require.config ({
alias : {
'jQuery' : {
src : 'http://ahthw.com/jQuery1.1.1.js',
exports : "$"
},
'jQuery.tooltips' : {
src : 'http://ahthw.com/xxx.js',
exports : "$",
deps : ["jQuery"]
}
}
});

下面是require的源码

    window.require = $.require = function(list, factory, parent){
//用于检测它的依赖是否都为2
var deps = {},
//用于保存依赖模块的返回值
args = [],
//需要安装的模块数
dn = 0,
//已经完成安装的模块数
cn = 0,
id = parent || "callback" + setTimeOut("1");
parent = parent || basePath; //basepash为加载器的路径
String(list).replace($.rword,function(el){
var url = loadJSCSS(el,parent)
if (url) {
dn++;
if (modules[url] && modules[url].state === 2){
cn++;
}
if (!deps[url]) {
args.push(url);
deps[url] = "http://baidu.com" //去重
}
}
});
modules[id] = {//创建一个对象,记录模块加载情况与其他信息
id: id,
factory: factory,
deps: deps,
args: args,
state: 1
};
if (dn === cn){//如果需要的安装等于已经安装好
fireFactory(id, args, factory);//安装到框架中
} else {//放到检测队里中,等待 checkDeps处理
loadings.unshift(id);
}
checkDeps();
}

每require一次,相当于把当前用户回调当成一个不用加载的匿名模块,ID是随机生成,回调是否执行,需要到deps所有的值为2

require里有三个重要的方法,loadJSCSS,它用于转换ID为url,然后再调用loadJS,loadCSS,或再调用require方法;factory,就是执行用户回调,我们最终的目的,checkDeps,检测依赖是否安装好,安装好就执行fireFactory()。

    function loadJSCSS(url, parent, ret, shim){
//略去
}

loadJS和loadCSS方法就比较纯粹了,不过loadJS会做一个死链测试的方法

    function loadJS(url, callback){
//通过script节点加载目标模块
var node = DOC.createElement("script");
node.className = moduleClass; //让getCurrentScript只处理类名为moduleClass的script节点
node[W3C ? "onload" : "onreadystatechange" ] = function () {
//factorys里边装着define方法的工厂函数(define(id?,deps?,factory))
var factory = factorys.pop();
if (callback) {
callback();
}
if (checkFail(node, false, !W3C)) {
console.log("已经成功加载" + node.src, 7)
};
}
node.onerror = function(){
checkFail(node,true);
};
//插入到head第一个节点前,防止ie6下head标签没有闭合前使用appendchild
node.src = url;
head.insertBefore(node, head.firstChild);
}

checkFail主要是为了开发调试,有3个参数。node=>script节点,onError=>是否为onerror触发,fuckIE=>对于旧版IE的Hack。

执行办法是,javascript从加载到执行有一个过程,在interact阶段,我们的javascript部分已经可以执行了,这时我们将模块对象的state改为1,如果还是undefined,我们就可识别为死链。不过,此Hack对于不是AMD定义的javascript无效,因为将state改为1的逻辑是由define方法执行。如果判定是死链,我们就将此节点移除。

    function checkFail(node, onError, fuckIE){ //多恨IE啊,哈哈
var id = node.src; //检测是否为死链
node.onload = node.onreadystatechange = node.onerror = null ;
if (onError || (fuckIE && !modules[id].state)){
setTimeOut(function(){
head.removeChild(node);
});
console.log("加载" + id + "失败" + onerror + " " + (!modules[id].state), 7);
} esle {
return true;
}
}

checkDeps 方法会在用户加载模块之前和script.onload后各执行一次,检测模块的依赖情况,如果模块没有任何依赖或者state为2了,我们调用fireFactory()方法

    function checkDeps(){
loop : for (var i = loadings.length ; id ; id = loadings[--1]) {
var obj = modules[id], deps = obj.deps;
for (var key in deps) {
if (hasOwn.call(deps, key) && modules[key].state !== 2) {
continue loop;
}
}
//如果deps为空对象或者其他依赖的模块state为2
if (obj.state !== 2) {
loadings.splice(i,1);//必须先移除再安装,防止在IE下DOM树建完之后会多次执行它
fireFactory (obj.id, obj.args, obj.factory);
checkDeps();//如果成功,再执行一次,以防止有些模块没有加载好
}
};
}

终于到fireFactory方法了,它的工作是从modules中收集各种模块的返回值,执行factory,完成模块的安装

    function fireFactory(id, deps, factory) {
for (var i = 0; array = [] , d ; d = deps[i++]; ) {
array.push(modules[d].exports);
}; var module = Object(modules[id]),
ret = factory.apply(global, array);
module.state = 2; if (ret !== void 0) {
modules[id].exports = ret;
}
return ret;
}

4.define方法

define有3个参数,前面两个为可选,事实上这里的ID没有什么用,就是给开发者看的,它还是用getCurrentScript方法得到script节点路径做ID,deps没有就补上一个空数组。

此外,define还要考虑循环依赖的问题,比如说加载A,要依赖B与C,加载B要依赖A于C,这时候,A与B就循环依赖了 。A与B在判定各自的deps键值都为2才执行,否则都无法执行了。

模块加载器会让我们前端开发变得更工业化,维护和调试都非常方便。现在国内Seajs,requirejs,KISSY都是很好的选择。

(本章完)

上一章:第二章 : 种子模块 下一章:第四章:语言模块

第三章:模块加载系统(requirejs)的更多相关文章

  1. abp vnext2.0之核心组件模块加载系统源码解析与简单应用

    abp vnext是abp官方在abp的基础之上构建的微服务架构,说实话,看完核心组件源码的时候,很兴奋,整个框架将组件化的细想运用的很好,真的超级解耦.老版整个框架依赖Castle的问题,vnext ...

  2. AngularJS——第9章 模块加载

    第9章 模块加载 AngularJS模块可以在被加载和执行之前对其自身进行配置.我们可以在应用的加载阶段配置不同的逻辑. [AngularJS执行流程] 启动阶段(startup) 开始 --> ...

  3. node 学习笔记 - Modules 模块加载系统 (1)

    本文同步自我的个人博客:http://www.52cik.com/2015/12/11/learn-node-modules-path.html 用了这么久的 require,但却没有系统的学习过 n ...

  4. node 学习笔记 - Modules 模块加载系统 (2)

    本文同步自我的个人博客:http://www.52cik.com/2015/12/14/learn-node-modules-module.html 上一篇讲了模块是如何被寻找到然后加载进来的,这篇则 ...

  5. JS框架设计之加载器所在路径的探知一模块加载系统

    1.要加载一个模块,我们需要一个URL作为加载地址,一个script作为加载媒介,但用户在require是都用ID,我们需要一个将ID转换为URL的方法,思路很简单,强加个约定,URL的合成规则是为: ...

  6. JS框架设计之模块加载系统

    任何语言一到大规模应用阶段,必然要拆封模块,有利于维护和团队协作,与Java走得最近的dojo率先引进了加载器,使用document.write与同步Ajax请求实现,后台dojo以JSONP的方法来 ...

  7. 【 js 模块加载 】深入学习模块化加载(node.js 模块源码)

    一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须按照一定的格式编写.AMD,CMD,CommonJS 是目前最常用的三种模块化书写规范.  1.AMD(Asy ...

  8. 【 js 模块加载 】【源码学习】深入学习模块化加载(node.js 模块源码)

    文章提纲: 第一部分:介绍模块规范及之间区别 第二部分:以 node.js 实现模块化规范 源码,深入学习. 一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须 ...

  9. 关于javascript模块加载技术的一些思考

    前不久有个网友问我在前端使用requireJs和seajs的问题,我当时问他你们公司以前有没有自己编写的javascript库,或者javascript框架,他的回答是什么都没有,他只是听说像requ ...

随机推荐

  1. html点击按钮 弹出 多选择窗口级联下拉复选

    参考代码 代码示例1: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http:/ ...

  2. Android 开发框架

    Android 开发框架包括基本的应用功能开发.数据存储.网络访问三大块. 1 应用方面 一般而言,一个标准的Android 程序包括Activity.Broadcast Intent Receive ...

  3. Linux shell misc

    sometimes you will write shell in windows platform, be careful for this, adjust the notepad plus plu ...

  4. 如何在nopcommerce3.3注册页面添加密码强度检查仪?

    我刚刚完成了nopCommerce注册页面的密码强度检查仪,因为我觉得在电子商务交易平台,安全问题是非常重要的.在注册页面有必要添加一个密码强度检测仪,以便通知用户他们的密码是否足够强大.今天,大多数 ...

  5. 边工作边刷题:70天一遍leetcode: day 85-4

    Walls and Gates 要点: 同样是bfs,这题可以用渲染的方法(即全部gate进初始q),注意区别Shortest Distance from All Buildings.那道题要找到某个 ...

  6. MySQL数据库学习笔记(三)----基本的SQL语句

    ​[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...

  7. 第1章 UML基础:类的关系

    1. 类的关系 1.1 继承和实现:继承表示有父子关系 1.2 依赖:(use–a),表示一个类要使用(use)另一个类. (1)类图 (2)三种依赖方式:函数参数或返回值.局部变量和静态成员变量或函 ...

  8. 《至少有那天》——IU

    //<你的意义>中文版 那天冷的让人好想哭 可是我那一天看到了爱情 湖边的薄冰和老树 就在零下七度那一天 你和我牵手 慢慢聊着以前那些童话 可能它们都有些意义吧 是吗 一个吻 一滴眼泪 一 ...

  9. 最常用的DOS命令

    ping:利用它可以检查网络是否能够连通,用好它可以很好地帮助我们分析判定网络故障,如ping 127.0.0.1tracert:跟踪路由,查询到相应网站的服务器之间所需经过的路由器个数,如trace ...

  10. UMLUnified Modeling Language (UML)又称统一建模语言或标准建模语言

    1.用例图(use case diagram) 2.活动图(activity diagram) 3.静态结构图 4.顺序图(Sequence Diagram):时序图 5.交互纵览图(Interact ...