JS 模块化规范:CommonJS, AMD, CMD, UMD, ESM

模块化开发就像工厂生产手机一样,用多个模块组成一个完整的应用程序。JS 模块化开发有比较长的历史,早期流行 命名空间 的开发思想,到后来有了一些模块化开发的规范,最先是 CommonJS (诞生于 NodeJS 社区,但这是在本地用的,并不适合浏览器端),后来 AMD、CMD、UMD、ESM 等规范相继诞生。因为 JS 并未提供一种原生的、语言级别的模块化开发模式,而是将模块化的方法交给开发者来实现,所以这些规范的诞生,让用 JS 进行模块化开发变得规范。这是我的学习笔记,记录各种 JS 模块化规范,它们之间有什么区别。

The Module Pattern,模块模式,也译为模组模式,是一种通用的对代码进行模块化组织与定义的方式。这里所说的模块(Modules),是指实现某特定功能的一组方法和代码。许多现代语言都定义了代码的模块化组织方式,比如 Golang 和 Java,它们都使用 package 与 import 来管理与使用模块,而目前版本的 JavaScript 并未提供一种原生的、语言级别的模块化组织模式,而是将模块化的方法交由开发者来实现。因此,出现了很多种 JavaScript 模块化的实现方式,比如,CommonJS Modules、AMD 等。

命名空间

例如一个库,有 “库名.类别名.方法名”

1
2
3
var NameSpace = {};
NameSpace.type = NameSpace.type || {};
NameSpace.type.method = function () {};

如代码所示,有一个 NameSpace 等于一个对象,在 NameSpace 下面有某一个类型,某一个类型也是一个对象,如果需要在这个类型里面添加一个方法的话,你就可以通过这种方式去定义。

代码 NameSpace.type = NameSpace.type || {}; 我们可以看到在定义 NameSpace.type 的时候,会判断 如果 NameSpace.type 已被定义,那么就继续是原来的,如果他没有被定义,那么就是新的对象。在一个项目开始前,团队可以约定,谁用哪些命名空间、总的命名空间,通过这种方式,就可以避免命名空间重复覆盖的问题。但如果命名空间被覆盖了,是检测不到的,这是弊端之一。另外一个弊端是,当你想调用某个方法时必须记住它完整的路径名,这样是非常不方便的,所以 命名空间 这种方式还是存在许多弊端的。

例如:MY_APP.IndexView.NavbarController.RightArea.UserProfile.UpdateUserName("...")

YUI2 大量使用了 命名空间 的方式,后来 YUI3 推出了一种沙箱机制(Sandbox),通过这种沙箱机制,暂时解决了命名空间路径很长的问题

1
2
3
4
5
6
7
8
9
10
11
12
YUI.add('hello', function (Y) {
Y.hello = function () {
Y.log('Hello World!');
};
}, '3.4.0', {
requires: ['harley-davidson', 'mt-dew']
});

YUI().use('hello', function (Y) {
// Module hello will be available here.
Y.hello();
});

可以在 GitHub 上了解 YUI.add 这个方法:传送门

CommonJS

再后来 CommonJS 出现了

CommonJS 规定 (Modules/1.1.1)

  • 一个文件为一个模块
  • 使用 exports.xxx = ...module.exports = {...} 暴露模块

在一个模块内的变量是不能被另一个模块直接访问的。也就是说在一个文件中,定义的一个变量,在另一个文件中是不能直接访问的,如果需要访问这个变量,则需通过 module.exports 这个 API (暴露模块接口) 来暴露它,让外界能够访问这些东西

  • 使用 require(...) 方法来引入一个模块
  • require(...) 是同步执行

CommonJS Modules/1.1.1 详细规范:传送门

CommonJS 在 NodeJS 环境用,不适用于浏览器端

CommonJS 示例

1
2
3
4
5
6
7
8
9
10
// a.js
let myFunc = function (msg) {
console.log('Hello World! ' + msg);
};

exports.helloWorld = myFunc;

// b.js
const a = require('./a.js');
a.helloWorld('Date: 2018/7/3');

命令行执行 node b.js 结果: Hello World! Date: 2018/7/3

1
2
3
4
5
6
7
8
9
10
11
12
// foo.js
exports.a = function (){
console.log('Hello World!');
}

module.exports = {a: 2}
exports.a = 1

// test.js
let x = require('./foo');

console.log(x.a)

命令行执行 node test.js 结果: 2

AMD

全称 Asynchronous module definition(异步模块定义)

AMD 规定

  • 使用 define(...) 定义一个模块
  • 使用 require(...) 加载一个模块(和 CommonJS 规范是相同的方法名,注意区分 CommonJS 和 RequireJS
  • 依赖前置,提前执行

AMD 详细规范:传送门

RequireJS 是 CMD 的一种实现

AMD 示例

因为 RequireJS 实现了 AMD 规范。所以下面的例子也借助 RequireJS 去实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
define(
// 模块名
"alpha",
// 依赖
["require", "exports", "beta"],
// 模块输出
function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
}
);

RequireJS 通过上面的方式定义一个模块,除此之外,还有另外一种方式:

["a", "b", "c", "d", "e"] 对应着 function (a, b, c, d, e) { 方法的参数,在这个方法中可以直接使用这些依赖

1
2
3
4
5
6
7
8
define(
["a", "b", "c", "d", "e"],
function (a, b, c, d, e) { // 在最前面声明并初始化了要用到的所有模块
if (false) { // 即便不会调用到某个模块 c,但 c 还是被提前执行了
c.foo();
}
}
)

另外,因为 AMD 和 CommonJS 规范拥有相同的方法名 require,所以容易将 RequireJS(AMD) 和 CommonJS 记混,注意区分。

CMD

全称 Common Module Definition(通用模块定义)

CMD 规定

  • 一个文件为一个模块
  • 使用 define(...) 定义一个模块(和 AMD 相似)
  • 使用 require(...) 加载一个模块(和 AMD 相似)
  • 尽可能懒执行(和 AMD 的不同点)

CMD 详细规范:传送门

SeaJS 是 CMD 的一种实现

CMD 示例

1
2
3
4
5
6
7
8
9
10
11
define(function(require, exports, module) {
var $ = require('jquery');
var Spinning = require('./spinning');

// 通过 exports 对外提供接口
// 注:不能直接对 exports 赋值,例如:exports = {...}
exports.doSomething = ...

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

CMD 和 AMD 的最显著区别 “as lazy as possible”

  1. CMD 推崇 as lazy as possible(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。

对于依赖的模块,AMD 是提前执行,CMD 是延迟执行,两者执行方式不一样,AMD 执行过程中会将所有依赖前置执行,也就是在自己的代码逻辑开始前全部执行;而 CMD 如果 require 但 整个逻辑并未使用这个依赖 或 未执行到逻辑使用它的地方前 不会执行。

不过 RequireJS 从 2.0 开始,也能改成延迟执行(根据写法不同,处理方式不同)

  1. CMD 推崇依赖就近,AMD 推崇依赖前置。

虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// CMD
define(function(require, exports, module) {
var a = require('./a');
a.doSomething();
// 此处略去 100 行
var b = require('./b'); // 依赖可以就近书写
b.doSomething();
// ...
});

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
//...
});

UMD

全称 Universal Module Definition(万能模块定义),从名字就可以看出 UMD 做的是大一统的工作。Webpack 打包代码就有 UMD 这个选项。

它会做三件事情:

  • 判断是否支持 AMD
  • 判断是否支持 CommonJS
  • 如果都不支持,使用全局变量

There are many techniques under UMD. My favourite UMD technique is returnExports. Here is example code on how to create math module using returnExport pattern. Place this code in “math.js” file

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
39
40
41
42
43
// if the module has no dependencies, the above pattern can be simplified to
(function (root, factory) {

// Environment Detection

// 对应上述的三个步骤
if (typeof define === 'function' && define.amd) {
// 1.判断是否支持 AMD
// 如果 define 这个方法是被定义 并且 define 这个方法是 AMD 的规范,那就把 factory 这个模块实体用 define 方法以 AMD 的规范 定义
define([], factory); // [] 是依赖,factory 是模块实体
} else if (typeof exports === 'object') {
// 2. 判断是否支持 CommonJS
// 如果 exports 是等于一个对象,则表明是在 Node 环境中运行,则支持 CommonJS,那就用 module.exports 暴露整个模块实体
module.exports = factory();
} else {
// 3. 如果都不支持,使用全局变量
// Browser globals (root 即是 window)
root.returnExports = factory();
}
}(this, function () {

// Module Defination

var sum = function(x, y){
return x + y;
}

var sub = function(x, y){
return x - y;
}

var math = {
findSum: function(a, b){
return sum(a,b);
},

findSub: function(a, b){
return sub(a, b);
}
}

return math;
}));

ES Module (ESM)

全称 ECMAScript Module

ESM 现在比较流行,随着 ESM 规范的普及,开发过程越来越多地中使用 ESM 的模块化规范,例如: vue-loader v13.0.0 Now uses ES modules internally to take advantage of webpack 3 scope hoisting.

ESM 规定

  • 一个文件为一个模块
  • 引入模块用 import 关键字 或 import(...) 方法
  • 暴露模块用 export 关键字(没有 s,注意和 CommonJS 的 exports.xxx = ...module.exports = {} 区分)

ESM 示例

import
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Default exports and named exports
import theDefault, { named1, named2 } from 'src/mylib';
import theDefault from 'src/mylib';
import { named1, named2 } from 'src/mylib';

// Renaming: import named1 as myNamed1
import { named1 as myNamed1, named2 } from 'src/mylib';

// Importing the module as an object
// (with one property per named export)
import * as mylib from 'src/mylib';

// Only load the module, don't import anything
import 'src/mylib';
export
1
2
3
4
5
6
7
export var myVar1 = '';
export let myVar2 = '';
export const MY_CONST = '';

export function myFunc() { }
export function* myGeneratorFunc() { }
export class MyClass { }
1
2
3
4
5
6
7
8
9
10
11
const MY_CONST = '';
function myFunc() {}

export { MY_CONST, myFunc };
export { MY_CONST as THE_CONST, myFunc as theFun };

export * from 'src/other_module';
export { foo, bar } from 'src/other_module';

// Export other_module's foo as myFoo
export { foo as myFoo, bar } from 'src/other_module';
export default
1
2
3
4
5
6
7
8
9
10
11
export default 123;
export default function (x) {
return x;
}
export default x => x;
export default class {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
实战
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// a.js
let helloWorld = function (msg) {
console.log('Hello World! ' + msg);
};

export {helloWorld};

// b.js
import {helloWorld} from './a.js';
helloWorld('Date: 2018/7/3');

// b.js
import {helloWorld as myTool} from './a.js';
myTool('Date: 2018/7/3');

// b.js
import * as a from './a.js';
a.helloWorld('Date: 2018/7/3');
1
2
3
4
5
6
7
8
9
10
11
// a.js
let helloWorld = function (msg) {
console.log('Hello World! ' + msg);
};

export default helloWorld;

// b.js
import helloWorld from './a.js';

helloWorld('Date: 2018/7/3');

执行上面的 b.js 都会输出相同的结果 Hello World! Date: 2018/7/3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// my-lib.js
let helloWorld = function (msg) {
console.log('Hello World! ' + msg);
};

let sayHello = function () {
console.log('Hello!');
};

let sayBye = function () {
console.log('Bye');
};

export {sayHello, sayBye};

export default helloWorld;

// action.js
import helloWorld, {sayHello as afterHelloWorld, sayBye} from './my-lib.js';

helloWorld('Date: 2018/7/3');
afterHelloWorld();
sayBye();

执行上面的 action.js:

1
2
3
4
5
babel-node action.js

Hello World! Date: 2018/7/3
Hello!
Bye

让 Node 环境支持 ESM

使用 Node v8.11.3

例如上面示例代码中,直接命令行 node action.js 会报错:SyntaxError: Unexpected token import

命令行执行 node --experimental-modules action.js 会出现提示 ExperimentalWarning: The ESM module loader is experimental.

可以使用 babel 让 Node 环境支持 ESM 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
cd [项目目录]

npm init
npm install --save babel-preset-latest babel-cli
touch .babelrc

# 编辑 .babelrc
{
"presets": ["latest"]
}

npm install babel-cli -g
babel-node action.js

动态 import()

传送门:Native ECMAScript modules: dynamic import()

Dynamic import() brings us the additional power to use the ES modules in an asynchronous way. To load them dynamically or conditionally depending on our needs, which gives us the ability to create even more advantage apps faster and better.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// STATIC
import './a.js';

import b from './b.js';
b();

import {c} from './c.js';
c();

// DYNAMIC
import('./a.js').then(()=>{
console.log('a.js is loaded dynamically');
});

import('./b.js').then((module)=>{
const b = module.default;
b('isDynamic');
});

import('./c.js').then(({c})=>{
c('isDynamic');
});

webpack 支持

  • AMD (RequireJS)
  • ES Module (推荐的)
  • CommonJS

虽然模块化规范有很多,但是只要掌握了 ES Module 和 CommonJS 这两种规范,那么使用 webpack 就基本没什么问题了,至于 AMD, CMD, UMD 了解一下足矣。

本站文章除注明转载外均为原创,未经允许不要转载哇. ヾ(゚ー゚ヾ) http://qwqaq.com/b8fd304a.html
分享到