CommonJS、AMD、CMD、ES6 Module

791 阅读9分钟

日期:2021年11月2日

Javascript模块化编程(一):模块的写法——阮一峰

Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。

但是,Javascript不是一种模块化编程语言,它不支持""(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版,将正式支持"类"和"模块",但还需要很长时间才能投入实用。)

——2012/10/26

JS模块编程-阮一峰.jpeg

JS 模块化编年简史

JS模块编年史.jpeg

CommonJS ➡️ NodeJs ➡️ RequireJs(AMD) ➡️ SeaJs(CMD) ➡️ ES6 module

CommonJS、AMD、CMD、ES6区别

名称CommonJsAMDCMDES6 Module
全称CommonJsAsynchronous Module DefinitionCommon Module DefinitionECMAScript 6.0 / 2015
同步/异步同步异步异步
实现实例NodeJSRequireJSSeaJS(淘宝)JavaScript
运行环境服务端浏览器浏览器前后端
特点1. 所有代码都运行在模块作用域,不会污染全局作用域;
2. 模块是同步加载的,即只有加载完成,才能执行后面的操作;
3. 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存;
4. CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。
依赖前置,提前执行。在define方法里传入的依赖模块(数组),会在一开始就下载并执行。依赖就近,延迟执行。只有到require时依赖模块才执行。

❓牺牲性能换取开发便利?
(对比CommonJS)
1. CommonJS模块是运行时加载,ES6 Module是编译时输出接口;
2. CommonJS加载的是整个模块,将所有的接口全部加载进来,ES6 Module可以单独加载其中的某个接口;
3. CommonJS输出是值的拷贝,ES6 Module输出的是值的引用,被输出模块的内部的改变会影响引用的改变;
4. CommonJS this指向当前模块,ES6 Module this指向undefined;
5. 目前浏览器对ES6 Module兼容还不太好,我们平时在webpack中使用的export/import,会被打包为exports/require。

CommonJS

概述

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

如果想在多个文件分享变量,必须定义为global对象的属性。

global.warning = true;

上面代码的warning变量,可以被所有文件读取。当然,这样写法是不推荐的。

每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过module.exports输出变量x和函数addX

require方法用于加载模块。

var example = require('./example.js');

console.log(example.x); // 5
console.log(example.addX(1)); // 6

exports变量

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。

exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

注意,不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系。

exports = function(x) {console.log(x)};

上面这样的写法是无效的,因为exports不再指向module.exports了。

下面的写法也是无效的。

exports.hello = function() {
  return 'hello';
};

module.exports = 'Hello world';

上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。

这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。

module.exports = function (x){ console.log(x);};

如果你觉得,exportsmodule.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports

AMD规范与CommonJS规范的兼容性

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

AMD规范则是非同步加载模块,允许指定回调函数。

由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。

但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

AMD规范使用define方法定义模块,下面就是一个例子:

define(['package/lib'], function(lib){
  function foo(){
    lib.log('hello world!');
  }

  return {
    foo: foo
  };
});

AMD规范允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:

define(function (require, exports, module){
  var someModule = require("someModule");
  var anotherModule = require("anotherModule");

  someModule.doTehAwesome();
  anotherModule.doMoarAwesome();

  exports.asplode = function (){
    someModule.doTehAwesome();
    anotherModule.doMoarAwesome();
  };
});

require命令

阮一峰 知乎

require 的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值 。

AMD (Asynchronous Module Definition)

require.js的用法——阮一峰

AMD 规范的实现:

<script src="require.js"></script>
<script src="a.js"></script>

所以在引入 require.js 文件后,再引入的其它文件,都可以使用 define 来定义模块。

define(id?, dependencies?, factory)

id:可选参数,用来定义模块的标识,如果没有提供该参数,就使用 js 文件名(去掉拓展名)对于一个 js 文件只定义了一个模块时,这个参数是可以省略的。

dependencies:可选参数,是一个数组,表示当前模块的依赖,如果没有依赖可以不传。

factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次,返回值便是模块要导出的值。如果是对象,此对象应该为模块的输出值。

模块A可以这么定义:

// a.js
define(function(){
    var name = 'morrain'
    var age = 18
    return {
        name,
        getAge: () => age
    }
})
// b.js
define(['a.js'], function(a){
    var name = 'lilei'
    var age = 15
    console.log(a.name) // 'morrain'
    console.log(a.getAge()) // 18
    return {
        name,
        getAge: () => age
    }
})

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等到加载完成之后,这个回调函数才会运行。

RequireJS 的基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,它开始加载它依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回值是该模块导出的值。AMD 是 "Asynchronous Module Definition" 的缩写,意思就是"异步模块定义"。

CMD (Common Module Definition)

和 AMD 类似,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。

Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯觉得 AMD 规范是异步的,模块的组织形式不够自然和直观。于是他在追求能像 CommonJS 那样的书写形式。于是就有了 CMD 。

CMD 规范的实现:

<script src="sea.js"></script>
<script src="a.js"></script>

在引入 sea.js 文件后,再引入的其它文件,都可以使用 define 来定义模块。

// 所有模块都通过 define 来定义
define(function(require, exports, module) {

  // 通过 require 引入依赖
  var a = require('xxx')
  var b = require('yyy')

  // 通过 exports 对外提供接口
  exports.doSomething = ...

  // 或者通过 module.exports 提供整个接口
  module.exports = ...

})
// a.js
define(function(require, exports, module){
    var name = 'morrain'
    var age = 18

    exports.name = name
    exports.getAge = () => age
})
// b.js
define(function(require, exports, module){
    var name = 'lilei'
    var age = 15
    var a = require('a.js')

    console.log(a.name) // 'morrain'
    console.log(a.getAge()) //18

    exports.name = name
    exports.getAge = () => age
})

Sea.js 可以像 CommonsJS 那样同步的形式书写模块代码的秘诀在于: 当 b.js 模块被 require 时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到 require 这个关键字,提取所有的依赖项,然后加载,等到依赖的所有模块加载完成后,执行回调函数,此时再执行到 require('a.js') 这行代码时,a.js 已经加载好在内存中了。

ES6 Module

任何模块化,都必须考虑的两个问题就是导入依赖和导出接口。ES6 Module 也是如此,模块功能主要由两个命令构成:exportimportexport 命令用于导出模块的对外接口,import 命令用于导入其他模块导出的内容。

具体语法讲解请参考阮一峰老师的教程,示例如下:

// a.js

export const name = 'morrain'
const age = 18
export function getAge () {
    return age
}

//等价于
const name = 'morrain'
const age = 18
function getAge (){
    return age
}
export {
    name,
    getAge
}

使用 export 命令定义了模块的对外接口以后,其他 JavaScript 文件就可以通过 import 命令加载这个模块。

// b.js
import { name, getAge } from 'a.js'
export const name = 'lilei'
console.log(name) // 'morrain'
const age = getAge()
console.log(age) // 18

// 等价于
import * as a from 'a.js'
export const name = 'lilei'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

从上面的例子可以看到,使用 import 命令的时候,用户需要知道所要导入的变量名,这有时候比较麻烦,于是 ES6 Module 规定了一种方便的用法,使用 export default 命令,为模块指定默认输出。

// a.js
const name = 'morrain'
const age = 18
function getAge () {
    return age
}
export default {
    name,
    getAge
}

// b.js
import a from 'a.js'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18

显然,一个模块只能有一个默认输出,因此 export default 命令只能使用一次。同时可以看到,这时 import 命令后面,不需要再使用大括号了。

除了基础的语法外,还有 as 的用法、exportimport 复合写法、export * from 'a'import()动态加载 等内容,可以自行学习。

📝❓前面提到的 Node.js 已经默认支持 ES6 Module ,浏览器也已经全面支持 ES6 Module。至于 Node.js 和 浏览器 如何使用 ES6 Module,可以自行学习。

ES6 Module 和 CommonJS 的区别

太长了,不说了 看这里🔎

动态和静态

CommonJS和ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。

这里“动态”的含义是, 模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。

值拷贝和动态映射

在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;

而在ES6 Module中则是值的动态映射,并且这个映射是只读的。

www.cnblogs.com/tandaxia/p/…

参考:

  1. AMD-CMD-CommonJS三者间的异同

  2. 理解import、require、export、module.export

  3. 《编程时间简史系列》JavaScript 模块化的历史进程

  4. Javascript模块化编程(二):AMD规范——阮一峰

  5. AMD 和 CMD 的区别有哪些?——玉伯回答

  6. 与 RequireJS 的异同

  7. CMD 模块定义规范

  8. CommonJS——维基百科

  9. CommonJS规范——阮一峰

  10. 前端科普系列-CommonJS:不是前端却革命了前端

  11. 再次梳理AMD、CMD、CommonJS、ES6 Module的区别