今天遇到一个组件之间传递数据的问题,由此展开了对模块加载器新一轮的学习。在该文档里记录下这段学习经历,以便后查。

RequireJs 与 SeaJs 的区别

BTW: 如果你还是习惯在部署上线前把所有js文件合并打包成一个文件,那么seajs和requirejs其实对你来说并无区别。

与其说是这两个加载器之间的区别,倒不如说是AMDCMD规范之间的区别。这两种规范我都没仔细看过,先留下链接以待日后细究:


  1. 大家公认的最大区别在模块的执行顺序上。这里照搬一个帖子里的对比:
     define(function(require, exports, module) {
         console.log('require module: main');
    
         var mod1 = require('./mod1');
         mod1.hello();
         var mod2 = require('./mod2');
         mod2.hello();
    
         return {
             hello: function() {
                 console.log('hello main');
             }
         };
     });
    

    运行结果应该是顺序的(sea.js下的结果): require module: main require module: mod1 hello mod1 require module: mod2 hello mod2 helo main

    而不应该是异步的require.js下: reqire module: mod2 require module: mod1 require module: main hello mod1 hello mod2 helo main

    从上面的结果很容易看出:

    对于依赖的模块,AMD 是__提前执行__,CMD 是__延迟执行__。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.

    PS: 这里说的执行是指factory,也就是define中我们写的function的执行时机

  2. RequirejsSeajsAPI也有很多不同之处

    AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。

  3. 在试用RequireJS时发现,RequireJs推崇require的依赖声明提前,而非用时再声明依赖。
         // 方式一:依赖声明前置
         define(
             ['mod1', 'mod2', 'mod3'],
             function(require) {
                 .....
             }
         );
    
         // 方式二:依赖用时声明
         define(
             function(require) {
                 var mod1 = require('mod1');
                 ...
                 var mod2 = require('mod2');
                 ...
                 var mod3 = require('mod3');
                 ...
             }
         );
    

    这是因为使用方式二的话,RequireJS需要额外的扫描factory提取出依赖的模块声明,再进行加载,这比方式一来说显然多走一步,性能会有下降。但是就编程逻辑而言,显然方式二更贴切,因而RequireJS提供优化工具r.js在编译时可将方式二的依赖提前。之前看过seajs的源码,对于这种情况,seajs也需要用正则扫描代码,不知道spm是不是也有类似r.js的处理。

PS: 在 FIS2中,js 框架使用mod.js作为模块加载器使用。实际上mod.js不能算AMD或者CMD的任何一种。因为它并没有完全实现这两种规范的任何一种(比如依赖关系都是由fis生成并管理的。fis在编译会生成一个 map.json文件,里面存放了所有 js 的依赖关系资源ID),很多活儿都交给了fis。但单就模块加载来说,mod.js遵循AMD规范,也就是提前执行所有的模块。

PS:amdcmd方式各有优劣,但我们使用 fisp必然会用到mod.js,所以顺带学习了mod.js的源码。

RequireJs学习

配置

  • baseUrl 定义requirejs查找所有模块的跟路径,如果没有设置,则使用当前页面的路径。

    注意:该配置与data-main属性有冲突。如果使用data-main来加载第一个模块,requirejs会认为data-main的值除去模块名的部分为baseUrl,需小心使用。

  • paths 基于baseUrl设置模块相对路径的简写。example:

          require.config({
              paths: {
                 jquery: 'lib/jQuery/2.1.3/jquery-2.1.3'
             }
          });
    
          // 然后可以直接使用 jquery 来require
          var $ = require('jquery');
    
  • bundles 将几个模块绑定到一起,如果有一个触发加载,其他的一起加载
      requirejs.config({
          bundles: {
              'primary': ['main', 'util', 'text', 'text!template.html'],
              'secondary': ['text!secondary.html']
          }
      });
    
      require(['util', 'text'], function(util, text) {
          //The script for module ID 'primary' was loaded,
          //and that script included the define()'d
          //modules for 'util' and 'text'
      });
    
  • shim 用来声明一些未支持amd或者不需要支持amd的类库,比如jquery的插件,本来就不需要包装。shim与Polyfill的区别
    • 适用于未支持amd的库,对于已经支持的库(比如已经被define包裹),requirejs可能不能正常工作
    • 只是声明依赖,并不会触发load
    • example
        require.config({
            //baseUrl: 'src',
            paths: {
                jquery: 'lib/jQuery/2.1.3/jquery-2.1.3'
            },
            shim: {
                'unveil': {
                    dep: ['jquery'],
                    exports: 'jQuery.fn.unveil'
                }
            }
        });
      

    The shim config only sets up code relationships. To load modules that are part of or use shim config, a normal require/define call is needed. Setting shim by itself does not trigger code to load.

  • map path可以将路径映射为别名,不过有时候我们想对不同的模块使用相同的别名,这时就可以使用map配置。举个栗子:
          requirejs.config({
              map: {
                  'some/newmodule': {
                      'foo': 'foo1.2'
                  },
                  'some/oldmodule': {
                      'foo': 'foo1.0'
                  }
              }
          });
    

    文件目录结构如下:

    • foo1.0.js
    • foo1.2.js
    • some/
      • newmodule.js
      • oldmodule.js

    如果在some/newmodule.js中使用var foo = require('foo');,这时调用的是foo1.2.js。同理,如果some/oldmodule.js中使用上述代码,调用的是foo1.0.js

    另外map中可以使用全局通配符*

          requirejs.config({
              map: {
                  '*': {
                      'foo': 'foo1.2'
                  },
                  'some/oldmodule': {
                      'foo': 'foo1.0'
                  }
              }
          });
    

    上面的配置可以这么理解,除了在some/oldmodule.js中引用foo会调用foo1.0.js,其他的引用都指向foo1.2.js。 **注意: map中不能使用相对路径,只用于amd模块。

  • config 该属性用作给模块传递参数(例如后端模板打印在页面的参数)。
          require.config({
              config: {
                  // 模块名
                  'bar': {
                      // 需传递的参数
                      size: 'large'
                  }
              }
          });
    
          //bar.js,
          define(function (require, exports, module) {
              //Will be the value 'large'
              // 使用 module的 config 方法获取参数
              var size = module.config().size;
          });
    

    如果是传给package的参数则有所区别

          requirejs.config({
              // 将一个 API key传给 pixie 这个package,需传给package的 main.js而非直接传递给 package
              config: {
                  'pixie/index': {
                      apiKey: 'XJKDLNS'
                  }
              },
              // package声明
              packages: [
                  {
                      name: 'pixie',
                      main: 'index'
                  }
              ]
          });
    
  • 其他配置 还有一些其他的配置项,例如packages,waitSeconds等等比较简单的配置,这里就不赘述了。

  • 插件 requirejs的社区提供了一些比较好用的插件,比如加载html资源的text插件。直接使用paths配置来加载就好。

r.js

requirejs的编译打包工具。试用了一会,配置比较复杂、繁琐,不深究(因为有 fis)。r.js文档

在 fis 中使用AMD

何大师写的fis+amddemo

需要用到的fis插件

  • npm install fis-postprocessor-amd -g
  • npm install fis-postpackager-autoload -g
  • npm install fis-packager-depscombine -g

mod.js源码阅读

  1. 依赖处理(fis参与)

    前面说过,mod.js除去没有对依赖的处理之外,更接近AMD规范。首先我们需要了解下fis如何为mod.js提供模块间的依赖信息。

     require.resourceMap({
           "res": {
             "common:widget/log/log.js": {
               "url": "/static/common/widget/log/log.js"
             },
             "home:widget/page_module/index/index.js": {
               "url": "/static/home/widget/page_module/index/index.js",
               "deps": [
                 "home:widget/share/share.js",
                 "common:widget/log/log.js",
                 "common:widget/lib/tangram/tangram.js",
                 "common:widget/lib/jquery/jquery.js",
                 "common:widget/lib/jquery/jquery.qrcode.js"
               ]
             },
             "home:widget/ContentPlayer/ContentPlayer.js": {
               "url": "/static/home/widget/ContentPlayer/ContentPlayer.js"
             },
             "home:widget/UserMonitor/UserMonitor.js": {
               "url": "/static/home/widget/UserMonitor/UserMonitor.js"
             },
             "common:widget/clickMonitor/clickMonitor.js": {
               "url": "/static/common/widget/clickMonitor/clickMonitor.js"
             }
           }
         })
    

    这段代码就是fis生成的各模块的依赖信息,最后打在页面上,供mod.js使用。require.resourceMap是在mod.js中的函数,用于将依赖信息存在resMap这个对象中,供其他函数使用。

  2. define define的定义如下,挂载到全局。很显然,define只有一个作用:id和它对应的factory放到factoryMap。至于loadingMap,该对象里存储的是loadScript函数执行的毁掉函数–updateNeed,该函数用于检测未加载的依赖模块数,若模块数为0,则执行当前模块的factory
     define = function(id, factory) {
         factoryMap[id] = factory;
    
         var queue = loadingMap[id];
         if (queue) {
             for(var i = 0, n = queue.length; i < n; i++) {
                 queue[i]();
             }
             delete loadingMap[id];
         }
     };
    
  3. require require函数分为两个require函数和require.async函数。require函数是同步require,也是局部requirerequire.async函数是异步require,也是全局 require。这里的概念取自RequireJS,我们一个一个来看。
    • require函数
            require = function(id) {
                // alias直接返回id,我也不知道是什么鬼
                id = require.alias(id);
      
                var mod = modulesMap[id];
                // 如果模块的 factory 已经被执行且将执行结果(module.exports)放到modulesMap中,直接反回
                if (mod) {
                    return mod.exports;
                }
      
                //
                // init module
                //
                var factory = factoryMap[id];
                // 在 define定义时已经将factory放到 factoryMap里了,如果找不到,说明模块未被定义
                if (!factory) {
                    throw '[ModJS] Cannot find module `' + id + '`';
                }
                // 将 exports挂载到 module下
                mod = modulesMap[id] = {
                    exports: {}
                };
      
                //
                // factory: function OR value
                //
                // 如果 factory 是函数,执行之,否则当成 value 直接返回
                var ret = (typeof factory == 'function')
                        ? factory.apply(mod, [require, mod.exports, mod])
                        : factory;
      
                if (ret) {
                    mod.exports = ret;
                }
                return mod.exports;
            };
      

      从代码中可以看出,modulesMap用于存放模块的执行结果。

      同步 require 用于返回一个现有的模块,如果模块不存在,不允许去请求模块,必须抛出一个错误。

      至于局部 require,我觉得mod.js可能不存在这个概念,因为require函数应该可以直接在页面使用,待测试

    • require.async函数
        require.async = function(names, onload, onerror) {
                if (typeof names == 'string') {
                    names = [names];
                }
      
                for(var i = 0, n = names.length; i < n; i++) {
                    names[i] = require.alias(names[i]);
                }
      
                // 还未加载的模块
                var needMap = {};
                // 还未加载的模块的数量
                var needNum = 0;
      
                // 迭代加载依赖模块
                function findNeed(depArr) {
                    for(var i = 0, n = depArr.length; i < n; i++) {
                        //
                        // skip loading or loaded
                        //
                        var dep = depArr[i];
      
                        if (dep in factoryMap){
                            // check whether loaded resource's deps is loaded or not
                            var child = resMap[dep];
                            // deps 是 fis 提供的依赖信息中该模块依赖的模块的 id 或 url
                            if (child && 'deps' in child) {
                                findNeed(child.deps);
                            }
                            continue;
                        }
      
                        if (dep in needMap) {
                            continue;
                        }
      
                        needMap[dep] = true;
                        needNum++;
                        loadScript(dep, updateNeed, onerror);
      
                        var child = resMap[dep];
                        if (child && 'deps' in child) {
                            findNeed(child.deps);
                        }
                    }
                }
      
                // 检测未加载的依赖模块的数量,并在所有依赖模块加载完成后执行当前模块
                function updateNeed() {
                    if (0 == needNum--) {
                        var args = [];
                        for(var i = 0, n = names.length; i < n; i++) {
                            args[i] = require(names[i]);
                        }
      
                        onload && onload.apply(global, args);
                    }
                }
      
                findNeed(names);
                updateNeed();
            };
      

      require.async复杂一些,它迭代加载了依赖模块,并在所有依赖模块执行完成后,执行当前模块。

    显然,require.async可以在页面上执行,可以作为引入其他模块的main函数,所以它是全局 require

  4. 资源加载 mod.js还提供资源加载,但只能加载jscssRequireJs还可以加载图片、文本、页面模板(一种特殊的文本)、多媒体内容等。
     require.loadJs = function(url) {
         createScript(url);
     };
    
     require.loadCss = function(cfg) {
         if (cfg.content) {
             var sty = document.createElement('style');
             sty.type = 'text/css';
    
             if (sty.styleSheet) {       // IE
                 sty.styleSheet.cssText = cfg.content;
             } else {
                 sty.innerHTML = cfg.content;
             }
             head.appendChild(sty);
         }
         else if (cfg.url) {
             var link = document.createElement('link');
             link.href = cfg.url;
             link.rel = 'stylesheet';
             link.type = 'text/css';
             head.appendChild(link);
         }
     };
    
  5. others
    • 使用require.async引入的模块,追加在<head>最后,而封装在widget里的js模块,会在DOM 树的最后由fis的插件FISResource.class.php打在页面上。

Reference

Appendix

mod.js源码

/**
 * file: mod.js
 * ver: 1.0.8
 * update: 2014/11/7
 *
 * https://github.com/zjcqoo/mod
 */
var require, define;

(function(global) {
    var head = document.getElementsByTagName('head')[0],
        loadingMap = {},
        factoryMap = {},
        modulesMap = {},
        scriptsMap = {},
        resMap = {},
        pkgMap = {};



    function createScript(url, onerror) {
        if (url in scriptsMap) return;
        scriptsMap[url] = true;

        var script = document.createElement('script');
        if (onerror) {
            var tid = setTimeout(onerror, require.timeout);

            script.onerror = function() {
                clearTimeout(tid);
                onerror();
            };

            function onload() {
                clearTimeout(tid);
            }

            if ('onload' in script) {
                script.onload = onload;
            }
            else {
                script.onreadystatechange = function() {
                    if (this.readyState == 'loaded' || this.readyState == 'complete') {
                        onload();
                    }
                }
            }
        }
        script.type = 'text/javascript';
        script.src = url;
        head.appendChild(script);
        return script;
    }

    function loadScript(id, callback, onerror) {
        var queue = loadingMap[id] || (loadingMap[id] = []);
        queue.push(callback);

        //
        // resource map query
        //
        var res = resMap[id] || {};
        var pkg = res.pkg;
        var url;

        if (pkg) {
            url = pkgMap[pkg].url;
        } else {
            url = res.url || id;
        }

        createScript(url, onerror && function() {
            onerror(id);
        });
    }

    define = function(id, factory) {
        factoryMap[id] = factory;

        var queue = loadingMap[id];
        if (queue) {
            for(var i = 0, n = queue.length; i < n; i++) {
                queue[i]();
            }
            delete loadingMap[id];
        }
    };

    require = function(id) {
        id = require.alias(id);

        var mod = modulesMap[id];
        if (mod) {
            return mod.exports;
        }

        //
        // init module
        //
        var factory = factoryMap[id];
        if (!factory) {
            throw '[ModJS] Cannot find module `' + id + '`';
        }

        mod = modulesMap[id] = {
            exports: {}
        };

        //
        // factory: function OR value
        //
        var ret = (typeof factory == 'function')
                ? factory.apply(mod, [require, mod.exports, mod])
                : factory;

        if (ret) {
            mod.exports = ret;
        }
        return mod.exports;
    };

    require.async = function(names, onload, onerror) {
        if (typeof names == 'string') {
            names = [names];
        }

        for(var i = 0, n = names.length; i < n; i++) {
            names[i] = require.alias(names[i]);
        }

        var needMap = {};
        var needNum = 0;

        function findNeed(depArr) {
            for(var i = 0, n = depArr.length; i < n; i++) {
                //
                // skip loading or loaded
                //
                var dep = depArr[i];

                if (dep in factoryMap){
                    // check whether loaded resource's deps is loaded or not
                    var child = resMap[dep];
                    if (child && 'deps' in child) {
                        findNeed(child.deps);
                    }
                    continue;
                }

                if (dep in needMap) {
                    continue;
                }

                needMap[dep] = true;
                needNum++;
                loadScript(dep, updateNeed, onerror);

                var child = resMap[dep];
                if (child && 'deps' in child) {
                    findNeed(child.deps);
                }
            }
        }

        function updateNeed() {
            if (0 == needNum--) {
                var args = [];
                for(var i = 0, n = names.length; i < n; i++) {
                    args[i] = require(names[i]);
                }

                onload && onload.apply(global, args);
            }
        }

        findNeed(names);
        updateNeed();
    };

    require.resourceMap = function(obj) {
        var k, col;

        // merge `res` & `pkg` fields
        col = obj.res;
        for(k in col) {
            if (col.hasOwnProperty(k)) {
                resMap[k] = col[k];
            }
        }

        col = obj.pkg;
        for(k in col) {
            if (col.hasOwnProperty(k)) {
                pkgMap[k] = col[k];
            }
        }
    };

    require.loadJs = function(url) {
        createScript(url);
    };

    require.loadCss = function(cfg) {
        if (cfg.content) {
            var sty = document.createElement('style');
            sty.type = 'text/css';

            if (sty.styleSheet) {       // IE
                sty.styleSheet.cssText = cfg.content;
            } else {
                sty.innerHTML = cfg.content;
            }
            head.appendChild(sty);
        }
        else if (cfg.url) {
            var link = document.createElement('link');
            link.href = cfg.url;
            link.rel = 'stylesheet';
            link.type = 'text/css';
            head.appendChild(link);
        }
    };


    require.alias = function(id) {return id};

    require.timeout = 5000;

})(this);

Written with StackEdit.

⤧  Next post Service Worker 学习笔记