Love.Passion.Dream

require.js 是如何去加载模块

require.js 是一个可以把js代码模块化的一个js框架,国内也有一个很不错的sea.js,还有之前我们组自己开发的AceAplication,pyjs,aceme。一个富js的网页对js代码按照功能进行模块划分是很有必要的。其实最简单的模块划分也可以简单的把代码用不同函数构造出闭包封装起来。

function a(){
    var name = 'a';
    return {
        say: function(){
            alert(name);
        }
    };
}

var module = a();
a.say();

这也就算是最基本的一个模块划分吧。一个a模块就这么诞生了。当然这对于以后甚至现在已经产生的越来越复杂的webapp这是远远不够的,我们还需要更多的组织代码,优化架构的东西。比如有extjs,backbone的MVC,还有seajs,require.js的异步加载等等。最终客户端软件开发有的甚至没有的也都将出现在web中,我也一直相信web有着辉煌的未来,而且不会局限于当前的浏览器与JS。

额,有点扯了。不管未来怎么样,现在仍然是需要基于浏览器,基于JS。所以今天看了一下require.js的源代码,看一下它是如何去加载模块的。

require.js最基本的两个方法require和define,这也是有具体的AMD规范。但是这两个方法也有很多的共同之处。

require(['./jqurey'], function($){
    console.log($);
});

define(['./jqurey'], function($){
    console.log($);
    return {};
});

这两个方法这么使用都可以去成功加载jqurey,这也导致之前在开发的时候把define写成了require结果还都能正常执行,直到后面使用到模块暴露的方法之后才出现问题。define与require不同的就是它多出来一个接口的返回。所以定义一个模块其实只需要在最后用define返回接口列表就可以了。jqurey就这是这样在代码最后面家了一段代码,使得它也可以直接在require.js中使用。

// Expose jQuery as an AMD module, but only for AMD loaders that
// understand the issues with loading multiple versions of jQuery
// in a page that all might call define(). The loader will indicate
// they have special allowances for multiple jQuery versions by
// specifying define.amd.jQuery = true. Register as a named module,
// since jQuery can be concatenated with other files that may use define,
// but not use 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.
if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
    define( "jquery", [], function () { return jQuery; } );
}

不管是require还是define,在他们的前面都传入了他们所依赖的模块,那是怎么去异步加载这些模块的呢?

如果没有进行代码合并,那么每一个模块是按照一个js文件来划分的。这样请求一个模块就是需要异步去加载一个js。这样如何去加载一个js需要用什么办法呢,先看require.js的代码:

/**
 * Does the request to load a module for the browser case.
 * Make this a separate function to allow other environments
 * to override it.
 *
 * @param {Object} context the require context to find state.
 * @param {String} moduleName the name of the module.
 * @param {Object} url the URL to the module.
 */
req.load = function (context, moduleName, url) {
    var config = (context && context.config) || {},
        node;
    if (isBrowser) {
        //In the browser so use a script tag
        node = config.xhtml ?
                document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
                document.createElement('script');
        node.type = config.scriptType || 'text/javascript';
        node.charset = 'utf-8';
        node.async = true;

        node.setAttribute('data-requirecontext', context.contextName);
        node.setAttribute('data-requiremodule', moduleName);

        //Set up load listener. Test attachEvent first because IE9 has
        //a subtle issue in its addEventListener and script onload firings
        //that do not match the behavior of all other browsers with
        //addEventListener support, which fire the onload event for a
        //script right after the script execution. See:
        //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
        //UNFORTUNATELY Opera implements attachEvent but does not follow the script
        //script execution mode.
        if (node.attachEvent &&
                //Check if node.attachEvent is artificially added by custom script or
                //natively supported by browser
                //read https://github.com/jrburke/requirejs/issues/187
                //if we can NOT find [native code] then it must NOT natively supported.
                //in IE8, node.attachEvent does not have toString()
                //Note the test for "[native code" with no closing brace, see:
                //https://github.com/jrburke/requirejs/issues/273
                !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
                !isOpera) {
            //Probably IE. IE (at least 6-8) do not fire
            //script onload right after executing the script, so
            //we cannot tie the anonymous define call to a name.
            //However, IE reports the script as being in 'interactive'
            //readyState at the time of the define call.
            useInteractive = true;

            node.attachEvent('onreadystatechange', context.onScriptLoad);
            //It would be great to add an error handler here to catch
            //404s in IE9+. However, onreadystatechange will fire before
            //the error handler, so that does not help. If addEvenListener
            //is used, then IE will fire error before load, but we cannot
            //use that pathway given the connect.microsoft.com issue
            //mentioned above about not doing the 'script execute,
            //then fire the script load event listener before execute
            //next script' that other browsers do.
            //Best hope: IE10 fixes the issues,
            //and then destroys all installs of IE 6-9.
            //node.attachEvent('onerror', context.onScriptError);
        } else {
            node.addEventListener('load', context.onScriptLoad, false);
            node.addEventListener('error', context.onScriptError, false);
        }
        node.src = url;

        //For some cache cases in IE 6-8, the script executes before the end
        //of the appendChild execution, so to tie an anonymous define
        //call to the module name (which is stored on the node), hold on
        //to a reference to this node, but clear after the DOM insertion.
        currentlyAddingScript = node;
        if (baseElement) {
            head.insertBefore(node, baseElement);
        } else {
            head.appendChild(node);
        }
        currentlyAddingScript = null;

        return node;
    } else if (isWebWorker) {
        //In a web worker, use importScripts. This is not a very
        //efficient use of importScripts, importScripts will block until
        //its script is downloaded and evaluated. However, if web workers
        //are in play, the expectation that a build has been done so that
        //only one script needs to be loaded anyway. This may need to be
        //reevaluated if other use cases become common.
        importScripts(url);

        //Account for anonymous modules
        context.completeLoad(moduleName);
    }
};

没错,它也是用了向html文档中插入script标签的方法,在该标签中加入了一个用于唯一标记模块的属性data-requiremodule,这样当js被加载运行后js中如果调用了define这个方法,那么这个方法就回去获取这个属性从而该模块就算是成功加载完毕了。

不过我感觉在浏览器中总有这么一些方法让人觉得很奇怪,浏览器无法提供一些原始的api,但是为了达到某些目的,开发者们通过其他的方法是实现,但是这个方法的设计的初衷并不是用于该目的。就比如这个通过在文档中插入javascript标签,另外还有什么iframe实现跨域,插入表单实现post请求,还有其他很多这样的方法。嗯,这样好吗?

=====

回过头来也补充下seajs的模块依赖分析与加载方式。

在seajs中通过define,require方法传入一个方法,这个方法就是整个模块的内容,在js中我们可以获取的某一个方法的内容,比如说:

var module = function(){alert('hello world!')};
console.log(module);
//function(){alert('hello world!')};

然后seajs会通过正则表达式分析模块中的依赖,这样就可以实现模块的同步require了,所以你的seajs模块中可以向下面这么写(seajs通常就是这样使用de的),却不会出现异步加载js的问题:

defint(function(){
  var t = require('./t');
  t.test();
});

seajs中用于分析模块依赖关系的正则表达式是:

    var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g

不过这样的方法引来了很多争议,现在正在考虑是否只是在debug的模式下才使用这样的方式,然后通过spm打包后上线的代码不会进行这样的检测,如果需要按需异步加载的话使用这样的方式:

seajs.use('./t'function(t) {
  t.test();
});