彻底搞懂前端各种模块化规范

Javascript 中有两种源文件,一种叫做脚本,一种叫做模块。脚本是可以由浏览器或 Node 环境引入执行的,而模块只能由 Javascript 代码用 import 引入。
模块中包含:import声明,export声明、语句。脚本中包含的是语句。所以脚本和模块的区别仅仅在于是否包含 import 和 export。浏览器中的脚本通过script 标签引入,如果要引入模块,需要给 script 标签加上 type="module"

1
<script type ="module" src=""></script>

下面来区分各种模块规范。本文代码示例在此

CommonJS

主要用于服务端,同步加载模块,只有加载完成,才能执行后面的操作。
CommonJS 定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)

暴露模块

CommonJS 暴露模块有两个,exports 和 module.exports

1
2
module.exports = value
exports.a = value

那这两个有什么区别呢?

module.exports 和 exports 都是引用类型,require 引入的内容是 module.exports 指向的内存块内容,require 是看不到 exports 对象的,exports 只是 module.exports 的引用,如果改变了 exports 的引用(也就是指向了一个新的内存地址),require 的还是 module.exports 的内容,所以最好一直用 module.exports。

引入模块

1
require(xxx)

第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性。
查看 CommonJS Demo

AMD: 异步模块定义,依赖前置。

CommonJS 规范主要用于服务端,那客户端的呢,而由于 CommonJS 同步的局限性,导致浏览器不能使用 CommonJS 规范,所以 AMD 规范就出来了,用于异步加载模块。AMD 是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”,
模块的加载不影响后续执行,依赖模块加载的语句,都会放在一个回调函数里,等到该模块加载完之后回调函数才运行。
define(id?, dependencies?, factory)
第一个参数 id:字符串,表示模块的标识。
第二个参数 dependencies:数组,元素是依赖模块的id。
第三个参数 factory:回调函数,在依赖的模块加载成功后,会执行这个回调函数,参数是所有依赖模块的引用。

暴露模块

1
2
3
4
5
6
7
8
//定义没有依赖的模块
define(function(){
return xxx
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return xxx
})

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。RequireJS 通过 require 函数实现模块的加载。

引入模块

1
require([module], callback);

查看 AMD Demo

CMD: 通用模块定义,依赖就近。

CMD 是”Common Module Definition”的缩写,意思就是”通用模块定义”,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。

暴露模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})

引入模块

1
2
3
4
5
6
7
// 使用模块
define(function (require) {
var m1 = require('./module1')
var m2 = require('./module2')
m1.show()
m2.show()
})

查看 CMD Demo

CMD与AMD区别

共同点:

  • 都是为了实现浏览器 JavaScript 模块化开发。
  • AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
  • CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。
    区别:
  • 对于依赖的模块,AMD 是 提前执行,CMD 是延迟执行。不过从 RequireJS 2.0开始,也可以延迟执行。
  • AMD 推崇依赖前置,CMD 推崇依赖就近。

ES6模块

在ES6之前,主要的模块规范是 CommonJS 和 AMD 规范,ES6模块设计思想是尽量静态化,使得编译时就确定模块的依赖关系(静态加载),便于做静态代码分析。同时,静态加载也限制了模块加载在文件中所有语句之前,并且不能使用表达式和变量这种运行时才能得到结果的语法结构。CommonJS 和 AMD 都只能在运行时确定。ES6 模块化的出现,旨在成为浏览器和服务器通用的模块解决方案,

export声明

模块导出变量的方式有两种,一种是独立使用 export 声明,一种是在声明型语句(let, const, var, function, class等)前加上 export。

1
2
3
4
// 独立使用export声明。
export {a, b, c};
// 声明型语句前加export。
export var a = 1;

export 还可以默认导出,使用 export default 表示默认导出一个变量值。使用 export default 来导出时,对应的 import 不需要大括号。

1
2
3
4
5
export default function add() { // 输出
// ...
}

import add from 'add'; // 输入

export default 输出一个 default 的变量,所以后面不能跟变量声明语句。

1
2
3
4
5
6
7
8
9
10

// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;

要注意的是,export 导出的必须是一个接口,与模块内的变量是一一对应的关系。不能直接导出变量值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 报错
export 1;

// 报错
var m = 1;
export m;

// 正确
// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

import声明

1
2
3
import x from "./a.js"; // 导入默认值
import {a as x, modify} from './a.js'; // 引入模块中的变量
import * from "./a.js"; // 把模块中的所有变量以类似对象的方式引入

ES6模块与CommonJS模块区别

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

1
2
3
4
5
6
7
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

CommonJS 加载模块是加载整个对象,生成 _fs 对象,再从这三个对象上读取3个方法,因为只有运行时才能得到这个对象,所以称为“运行时加载”,而 ES6 模块

1
2
// ES6模块
import { stat, exists, readFile } from 'fs';

只会加载三个方法,别的不会加载,ES6 模块不是对象,对外接口是一种静态定义,所以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS 模块输出一个对象,结果被缓存,在第一次运行的时候加载一次,之后从缓存里读。JS 引擎在对脚本进行静态分析的时候,遇到模块加载命令 import,会先生成一个只读引用,等脚本真正执行的时候,才会去模块里执行。所以 ES6 模块是动态引用,不会缓存值。