记录 js 模块化的基础知识。

JavaScript模块化

模块化是指在解决某一个复杂问题或者一系列的杂糅问题时,依照一种分类的思维把问题进行系统性的分解之后加之处理。反映到槟城上,其就是一种将复杂系统分解为代码结构更加合理,可维护性更加高的,便于管理的方式。

可以想象一个巨大的系统代码,被整合优化分割成逻辑性很强的模块时,对于软件是一种何等意义的存在。对于软件行业来说:解耦软件系统的复杂性,使得不管多么大的系统,也可以将管理,开发,维护变得“有理可循”。

还有一些对于模块化一些专业的定义为:模块化是软件系统的属性,这个系统被分解为一组高内聚,低耦合的模块。那么在理想状态下我们只需要完成自己部分的核心业务逻辑代码,其他方面的依赖可以通过直接加载被人已经写好模块进行使用即可。

首先,既然是模块化设计,那么作为一个模块化系统所必须的能力:

  1. 定义封装的模块
  2. 定义新模块对其他模块的依赖
  3. 可对其他模块的引入支持

思想有了,然后来建立一个模块化的规范制度,不然各式各样的模块加载方式只会将局搅得更为混乱。那么在JavaScript中出现了一些非传统模块开发方式的规范 CommonJS、AMD(Asynchronous Module Definition)、CMD(Common Module Definition)及 UMD(Universal Module Definition)等。

概述 CommonJS、AMD、CMD、UMD

AMD(Asynchromous Mdoule Definition,异步模块定义)是RequireJS在推广过程中对模块定义的规范化产出;
CMD(Common Mdoule Definition,通用模块定义)是SeaJS在推广过程中对模块定义的规范化产出;
CommonJS 是服务端模块的规范,Node.js采用了这个规范;
UMD(Universal Module Definition,统一模块定义)

AMD与CMD的区别和联系

共同点:

  1. 两者都是异步加载的规范
  2. 两者都是适用于浏览器加载资源的规范
  3. 两种规范中都有兼容对方的写法

不同点:

  1. CMD 规范的写法与 CommonJS 规范的写法更加相近
  2. AMD 是提前执行,CMD 是延迟执行
    这里要区分以下”执行”和”加载”,先上代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // sea.js
    // 默认会按顺序依次传入3个指定的实参
    define(function(require, exports, module){
    var A = require('./a');
    A.run();
    var B = require('./b');
    B.run();
    });

    // require.js
    define(['./a', './b'], function(A, B){
    A.run();
    B.run();
    });

    两者都是并行加载模块,但是 require.js 会先执行/解析所有模块之后,再运行回调函数体,不管实际上回调函数体有没有用到某个所加载进来的模块;
    而seajs虽然也会提前并行加载完所有模块,但是它会延迟执行,也就是当在某一行使用到的时候再解析模块;

  3. AMD推崇依赖前置,CMD推崇依赖就近;
  4. AMD的API是一个当多个用,而CMD的API严格区分,简单纯粹,推崇职责单一;
    比如:AMD 的 require 区分全局 require 和局部 require,而且功能不同;而 CMD 里没有全局 require,只用局部 require;
  5. require.js 出自 dojo 加载器的作者 James Burke,sea.js 出自国内前端大师玉伯

关于两者的区别,玉伯如是说:

RequireJS 和 SeaJS 都是很不错的模块加载器,两者区别如下:

  1. 两者定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端
  2. 两者遵循的标准有差异。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者 API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
  3. 两者社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,而采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
  4. 两者代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。
  5. 两者对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便便捷。RequireJS 无这方面的支持。
  6. 两者的插件机制有差异。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Node 的方式一致:开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。

CommonJS规范

CommonJS定义的模块分为3部分:

  1. require 模块引用
  2. exports 模块导出
  3. module 模块本身

根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的
CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作;不适用于浏览器端
例如:NodeJS用于服务器端,2009年被创建,参照CommonJS规范实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// foobar.js
//私有变量
var test = 123;
//公有方法
function foobar () {
this.foo = function () {
// do someing ...
}
this.bar = function () {
//do someing ...
}
}
// exports对象上的方法和变量是公有的
var foobar = new foobar();
exports.foobar = foobar;

// test.js
// require方法默认读取js文件,所以可以省略js后缀
var test = require('./foobar.js').foobar;
test.bar();

CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,等待的时间就是硬盘加载资源的时间,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。

AMD和RequireJS

AMD

全称:Asynchromous Module Definition 规范

模块定义

注意⚠️:以下的define都是得由外部引入的AMD库实现的,原生JS并无该函数;

  1. AMD模块加载是异步的
  2. define(id?, dependencies?, factory);

    id: 模块标识,可以省略;若不存在则模块标识应该默认定义为在加载器中被请求脚本的标识。如果存在,那么模块标识必须为顶层的或者一个绝对的标识
    dependencies: 所依赖的模块,可以省略
    factory: 模块的实现,或者一个JavaScript对象

    • 定义无依赖的模块

      1
      2
      3
      4
      5
      define({
      add : function( x, y ){
      return x + y ;
      }
      });
    • 定义有依赖的模块

      1
      2
      3
      4
      5
      6
      7
      define(['alpha'], function(alpha){
      return {
      verb: function(){
      return alpha.verb() + 1;
      }
      }
      });
    • 定义数据对象模块

      1
      2
      3
      4
      define({
      users: [],
      members: []
      });
    • 具名模块

      1
      2
      3
      4
      5
      6
      7
      define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){
      exports.verb = function(){
      return beta.verb();
      // or:
      return require("beta").verb();
      }
      });

    ps:最好不要自己指定模块名字,而是交给优化工具去完成;否则当改变文件路径的时候,又需要去改动模块名字;

    • 包装模块(不含依赖)
      AMD规范允许输出模块兼容CommonJS规范,这时define方法如下:
      1
      2
      3
      4
      5
      6
      define(function(require, exports, module) {
      var a = require('a'),
      b = require('b');

      exports.action = function() {};
      } );

    ps:当没有依赖时,上面的3个形参是固定会传入的,只要在形参的个数大于1,3个实参就会按顺序传入;即使js代码经过uglify之后也是一样效果;
    不考虑多了一层函数外,格式和Node.js是一样的:使用require获取依赖模块,使用exports导出API。
    除了define外,AMD还保留一个关键字require。require 作为规范保留的全局标识符,可以实现为 module loader,也可以不实现

    • 包装模块(含依赖)
      AMD规范允许输出模块兼容CommonJS规范,这时define方法如下:
      1
      2
      3
      4
      5
      6
      define(['alpha', 'require'],function(alpha, require) {
      var a = require('a'),
      b = require('b');

      exports.action = function() {};
      } );
模块加载
1
require([module], callback)

AMD模块化规范中使用全局或局部的require函数实现加载一个或多个模块,所有模块加载完成之后的回调函数。
[module]:是一个数组,里面的成员就是要加载的模块;
callback:是模块加载完成之后的回调函数。
例如:加载一个math模块,然后调用方法 math.add(2, 3);

1
2
3
require(['math'], function(math){
console.log(math.add(1, 2));
});

RquireJS

RequireJS 是一个前端的模块化管理的工具库,遵循AMD规范,它的作者就是AMD规范的创始人 James Burke。所以说RequireJS是对AMD规范的阐述一点也不为过
RequireJS 的基本思想为:通过一个函数来将所有所需要的或者说所依赖的模块实现装载进来,然后返回一个新的函数(模块),我们所有的关于新模块的业务代码都在这个函数内部操作,其内部也可无限制的使用已经加载进来的模块。

RequireJS 的诞生解决了如下问题:

  1. 实现js文件的异步加载,避免网页失去响应
  2. 管理模块之间的依赖性,便于代码的编写与维护

RequireJS 只引入了三个全局变量

  1. require
  2. requirejs (requirejs === require)
  3. define
1
<script data-main='scripts/main' src='scripts/require.js'></script>

那么scripts下的main.js则是指定的主代码脚本文件,所有的依赖模块代码文件都将从该文件开始异步加载进入执行。

define用于定义模块,RequireJS要求每个模块均放在独立的文件之中。按照是否有依赖其他模块的情况分为独立模块和非独立模块。

  • 独立模块,不依赖其他模块,直接定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    define({
    method1: function(){},
    method2: function(){}
    });

    // 等价于
    define(function() {
    return {
    method1: function(){},
    method2: function(){}
    }
    });
  • 非独立模块,对其他模块有依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    define([ 'module1', 'module2' ], function(m1, m2) {
    //...
    });

    //或者
    //简单看了一下RequireJS的实现方式,其 require 实现只不过是提取 require 之后的模块名,将其放入依赖关系之中。
    define(function(require) {
    var m1 = require('module1'),
    m2 = require('module2');
    //...
    });
  • require方法调用模块
    在require进行调用模块时,其参数与define类似。

    1
    2
    3
    4
    5
    //在加载 foo 与 bar 两个模块之后执行回调函数实现具体过程。
    require(['foo', 'bar'], function(foo, bar) {
    foo.func();
    bar.func();
    } );

RequireJS定义模块文件或加载模块文件中的配置项:

  • 加载模块文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    require.config({
    paths : {
    "jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery"]
    }
    });
    require(["jquery","js/a"],function($){
    $(function(){
    alert("load finished");
    })
    });

    // 而且本地文件也可以进行指定alias
    require.config({
    paths : {
    "jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery"],
    "a": "js/a"
    }
    });
    require(["jquery","js/a"],function($){
    $(function(){
    alert("load finished");
    })
    });

    // 也可以为一份资源配置备用路径;若以上的cdn资源没有加载成功,可以使用备用的
    require.config({
    paths : {
    "jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery", 'js/jquery'],
    "a": "js/a"
    }
    });
    require(["jquery","js/a"],function($){
    $(function(){
    alert("load finished");
    })
    });
  • 全局配置
    上面的例子中重复出现了require.config配置,如果每个页面中都加入配置,必然显得十分不雅,requirejs提供了一种叫”主数据”的功能,例如如下指定了主文件:

    1
    <script src="//cdn.bootcss.com/require.js/2.3.3/require.js" defer async="true" data-main="./js/main"></script>

把require.config的配置加入到data-main指定的文件后:

  1. 就可以使每一个页面都使用这个配置,然后页面中就可以直接使用require来加载所有的短模块名;
  2. 所有加载文件的根路径默认为data-main所指定文件所在的路径;
    当然也可以自定义配置根路径:
    1
    2
    3
    4
    5
    6
    7
    8
    require.config({
        baseUrl: "js/lib",
        paths: {
          "jquery": "jquery.min",
          "underscore": "underscore.min",
          "backbone": "backbone.min"
        }
      });
  • 第三方模块
    配置对象中的shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。
    如果有一些库并没有遵循AMD规范,则也可以通过require.config兼容他们:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    require.config({
        shim: {
          'underscore':{
            exports: '_'
          },
          'backbone': {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone'
          }
        }
      });

RequireJS还拥有许多js插件,可在网上搜寻;

define 和 require 这两个定义模块,调用模块的方法合称为AMD模式;定义模块清晰,不会污染全局变量,清楚的显示依赖关系。AMD模式可以用于浏览器环境并且允许非同步加载模块,也可以按需动态加载模块。

官网(http://www.requirejs.org/)
API(http://www.requirejs.org/docs/api.html)

CMD和SeaJS

CMD

全称:Common Module Definition 规范
CMD是SeaJS 在推广过程中对模块定义的规范化产出
CMD和AMD的区别有以下几点:

  1. 对于依赖的模块,AMD是提前执行,CMD是延迟执行;但RequireJS从2.0开始,也可以改成延迟执行(根据写法不同,处理方式也不同)
  2. CMD推崇依赖就近,AMD推崇依赖前置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //AMD写法
    define(['./a','./b'], function (a, b) {
    //依赖一开始就写好
    a.mix();
    b.show();
    });

    //CMD写法
    define(function (requie, exports, module) {
    //依赖可以就近书写
    var a = require('./a');
    a.mix();

    if (...) {
    var b = requie('./b');
    b.show();
    }
    });

虽然 AMD也支持CMD写法,但依赖前置是官方文档的默认模块定义写法。
AMD的API默认是一个当多个用,CMD严格的区分推崇职责单一。例如:AMD里require分全局的和局部的。CMD里面没有全局的 require,提供 seajs.use()来实现模块系统的加载启动。CMD里每个API都简单纯粹

CMD模块定义规范

经常使用的API有define,require,require.async,exports,module.exports5个,其他有个印象就好,不用刻意去记;
与 RequireJS 的 AMD 规范相比,CMD 规范尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。通过 CMD 规范书写的模块,可以很容易在 Node.js 中运行;

UMD(Universal Module Definition)规范

UMD,统一模块定义,是AMD和CommonJS的糅合
AMD模块以浏览器第一的原则发展,异步加载模块。
CommonJS模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。
这迫使人们又想出另一个更通用的模式UMD (Universal Module Definition)。希望解决跨平台的解决方案。

UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
// methods
function myFunc(){};

// exposed public method
return myFunc;
}));

应用UMD规范的js文件其实就是一个立即执行函数。函数有两个参数,第一个参数是当前运行时环境,第二个参数是模块的定义体。在执行UMD规范时,会优先判断是当前环境是否支持AMD环境,然后再检验是否支持CommonJS环境,否则认为当前环境为浏览器环境( window )

让自定义插件兼容CommonJs/CMD/AMD和原生JS

模块标准

  • CommonJS
    CommonJS 有三个全局变量 module、exports 和 require。但是由于 AMD 也有 require 这个全局变量,故不使用这个变量来进行检测。
    如果想要对外提供接口的话,可以将接口绑定到 exports (即 module.exports) 上。

    1
    2
    3
    4
    5
    6
    function MyModule(){
    // ...
    }
    if(typeof module !== 'undefined' && typeof exports === 'Object'){
    module.exports = MyModule;
    }
  • CMD
    CMD 规范中定义了 define 函数有一个公有属性 define.cmd
    CMD 模块中有两种方式提供对外接口,一种是 exports.MyModule = ...,另一种是 return 进行返回

  • AMD
    AMD 规范中,define函数同样有一个公有属性 define.amd
    AMD 中的参数便是这个模块的依赖。它是返回一个对象,这个对象就作为这个模块的接口,故可如下:
    1
    2
    3
    4
    5
    6
    function MyModule() {}
    if (typeof define === 'function' && define.amd) {
    define(function(){
    return MyModule;
    });
    }

总结:
除了提供 AMDCMD 模块接口,还得提供原声的js接口
由于 AMDCMD 都可以使用 return 来定义对外接口,所以可合并成一句代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;(function(){
function MyModule(){}

var moduleName = MyModule;
if (typeof module !== 'undefined' && typeof exports === 'object') {
module.exports = mdouleName;
} else if (typeof define === 'function' && (define.amd || define.cmd)) {
define(function(){
return moduleName;
});
} else {
this.moduleName = moduleName;
}
}).call(function(){
return this || (typeof window !== 'undefined'?window: global);
});

更全面的:

1
2
3
4
5
6
7
8
9
10
11
12
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["MyLibrary"] = factory();
else
root["MyLibrary"] = factory();
})(this, function() {
//这个模块会返回你的入口 chunk 所返回的
});

参考链接