第三章:模块加载系统(requirejs)

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

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

1.AMD规范html

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

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

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

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

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

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的方法。思路很简单,约定为:app

    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都是很好的选择。

(本章完)

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

相关文章
相关标签/搜索