think of JsModule

模块导入导出的历史

JsModule 的演化经历

下面这张图可以清晰的看出,javascript module 演化的历史,由最初的 commonjs 到最终方案 esm,而现在正处于 umd -> esm 阶段.

commonjs

commonjs 的特性就是其导入不是在编译器编译时执行的,而是在代码执行时才实行的.其特性导致了两个特点: 动态导入和赋值复制.

  • 动态导入.

下面这段 js 代码完美诠释了此含义,在此判断为 true 的情况下的 commonjs,才会导入 selectivizr,并实行 selectivizr 中的脚本.

1
2
3
if(browser.desktop && browser.msie && browser.versionNumber < 9){
require('selectivizr');
}
  • 赋值复制.
1
2
3
4
5
6
7
8
9
// module.js
let count = 4;
function add() {
count++;
}
module.exports = {
count,
add
};
1
2
3
4
5
// index.js
const {count, add} = require('./module.js');
console.log('count:', count);
add();
console.log('count:', count);

上面这两段 js 代码完美诠释了此含义,其结果为:

count: 4
count: 4

可以看出 commonjs 对于导出的变量以及函数都是代码执行时直接复制其值,而不是连同引用一起导出,导致通过模块内部修改变量的值之后,在外部导入模块变量并没有发现其值发生变化.

  • 优势和劣势.

    • 优势:

      • 导入比较灵活;
      • NodeJS 模块导入导出完全采用 commonjs 模式,npm 上绝大部分的依赖库都会兼容 commonjs 模块导入导出,适用范围很广泛;
      • 同步模块加载;
      • 良好的团队维护,完备的社区/论坛;
    • 劣势:

      • 不支持静态分析,静态分析所带来的一系列福利不能在 commonjs 模块导入导出模式下实行;
      • 不能实行异步模块加载;

amd(cmd)

amd(cmd) 的适用范围很窄,受众面也远远没有 commonjs 和 esm 广泛,因为受限于第三方库的环境依赖(无论是 SeaJS,还是 RequireJS 都需要事先下载依赖).

  • 优势和劣势.

    • 优势:

      • 支持同步/异步模块加载,amd 近似于同步模块导入导出(与 commonjs 同步模块加载有着本质的不同),cmd 异步模块导入导出(与 esm 异步模块加载也有着本质的不同);
    • 劣势:

      • 不支持静态分析,静态分析所带来的一系列福利不能在 amd(cmd) 模块导入导出模式下实行;
      • 受限于第三方库的环境依赖;
      • 写法上很不友好;
      • 适用范围很窄,没有类 NodeJS、npm 以及 ECMAScript 标准这种受众面很广泛的’推手’推动;
      • 社区/论坛不成熟,维护一般;

umd

umd,全称: Universal Module Definition,其标准为: commonjsStrictGlobal以及returnExportsGlobal.

  • commonjsStrictGlobal.
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
37
38
// Uses CommonJS, AMD or browser globals to create a module. This example
// creates a global even when AMD is used. This is useful if you have some
// scripts that are loaded by an AMD loader, but they still want access to
// globals. If you do not need to export a global for the AMD case, see
// commonjsStrict.js.

// If you just want to support Node, or other CommonJS-like environments that
// support module.exports, and you are not creating a module that has a
// circular dependency, then see returnExportsGlobal.js instead. It will allow
// you to export a function as the module value.

// Defines a module "commonJsStrictGlobal" that depends another module called
// "b". Note that the name of the module is implied by the file name. It is
// best if the file name and the exported global have matching names.

// If the 'b' module also uses this type of boilerplate, then
// in the browser, it will create a global .b that is used below.

(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'b'], function (exports, b) {
factory((root.commonJsStrictGlobal = exports), b);
});
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
// CommonJS
factory(exports, require('b'));
} else {
// Browser globals
factory((root.commonJsStrictGlobal = {}), root.b);
}
}(typeof self !== 'undefined' ? self : this, function (exports, b) {
// Use b in some fashion.

// attach properties to the exports object to define
// the exported module properties.
exports.action = function () {};
}));
  • returnExportsGlobal.
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
// Uses CommonJS, AMD or browser globals to create a module.

// If you just want to support Node, or other CommonJS-like environments that
// support module.exports, and you are not creating a module that has a
// circular dependency, then see returnExports.js instead. It will allow
// you to export a function as the module value.

// Defines a module "commonJsStrict" that depends another module called "b".
// Note that the name of the module is implied by the file name. It is best
// if the file name and the exported global have matching names.

// If the 'b' module also uses this type of boilerplate, then
// in the browser, it will create a global .b that is used below.

// If you do not want to support the browser global path, then you
// can remove the `root` use and the passing `this` as the first arg to
// the top function.

(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports', 'b'], factory);
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
// CommonJS
factory(exports, require('b'));
} else {
// Browser globals
factory((root.commonJsStrict = {}), root.b);
}
}(typeof self !== 'undefined' ? self : this, function (exports, b) {
// Use b in some fashion.

// attach properties to the exports object to define
// the exported module properties.
exports.action = function () {};
}));

从源码中可以看出 umd 是对于 commonjs、Node(webpack中值枚举为commonjs2)、amd 以及 Browser globals 的并集,是实行兼容的一种模块导入导出模式.

  • 优势和劣势.

    • 优势:

      • 兼容的这几种模块导入导出都支持同步模块加载;
      • 导入比较灵活;
      • 导出的类型更丰富;
    • 劣势:

      • 不支持静态分析,静态分析所带来的一系列福利也不能在 umd 模块导入导出模式下实行;
      • 不能实行异步模块加载;

esm(ecmascript module)

模块导入导出的最终方案模式,也是现在 NodeJS、npm 以及 ECMAScript 标准这些受众面很广泛的’推手’主要推动的模块导入导出模式. 其导入是在编译器编译阶段,由此特性也导致了两个特点: 静态分析和赋值引用,与 commonjs 的特性与特点完全相反.

  • NodeJS ESM.

混用阶段,也就是 import 配合 module.exports,require 配合 export.

注意在 import 配合 module.export 这部分,import 必须导入 module.exports 导出的模块,不能导入 exports 导出的模块,由于还是以commonjs模块导出,那么esm导入就不能实行静态分析,只能整体导入.

1
2
3
4
5
6
//module.js
const count = 4;
//NodeJS ESM 必须使用 module.exports 导出
module.exports = {
count
};
1
2
3
4
5
//index.js
//以 commonjs 模块导出,esm 导入不能实行静态分析,只能整体导入.
//import {count} from './module.js';
import module from './module.js';
console.log(module.count);
  • 静态分析.

其导入是在编译器编译阶段,所以需要将所使用的模块都在所要导入文件的头部进行导入.可对其引入的值、函数或者模块可进行静态分析.

1
2
//module.js
export const count = 4;
1
2
3
4
//index.js
//对其引入的值、函数或者模块可进行静态分析
import {count} from './module.js';
console.log(count);
  • 赋值引用.
1
2
3
4
5
//module.js
export let count = 4;
export function add() {
count++;
}
1
2
3
4
5
//index.js
import {count, add} from './module.js';
console.log('count:', count);
add();
console.log('count:', count);

上面这两段 js 代码与 commonjs 赋值复制部分是同一个🌰,但是执行结果却是不相同的,其结果为:

count: 4
count: 5

可以看出 esm 对于导出的变量以及函数都是编译器编译时连同引用一起导出,导致通过模块内部修改变量的值之后,在外部导入模块变量的值也发生了改变.

  • 优势和劣势.

    • 优势:

      • 支持静态分析,静态分析所带来的一系列福利都可接收;
      • 可实行异步模块加载;支持动态导入 import();
      • 良好的团队维护,完备的社区/论坛;
    • 劣势:

      • 现阶段 NodeJS、npm 以及 ECMAScript 标准这些受众面很广泛的’推手’因历史、兼容、底层改动大等问题,实现的都不成熟,还需要 Webpack/Babel 等工具进行转译;