在使用JavaScript开发大型项目时,模块开发是一个必须考虑的问题,流行的js模块化规范有CommonJS、AMD、CMD和ES6模块,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。
1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
一、CommonJS、Node.js
在前端浏览器中并不支持该规范,主要用于服务器端,Node.js是CommonJS规范的主要实践者。我们用 module.exports 定义当前模块对外输出的接口(不推荐直接用exports),用 require 加载模块。代码如下:
//定义模块 math.js
const num = 0;
function add(a, b) {
return a + b;
}
module.exports = {
add:add,
num:num
}
//加载模块,参数包含路径,可省略.js
const math = require('/math');
math.add(2, 5);
//引用核心模块时,不需要带路径
const http = require('http');
CommonJS采用同步的方式加载模块,在服务器端,模块文件都存在本地磁盘,读取非常快,因此也不需要异步加载。但在浏览器端的话,则需要浏览器端发起请求到服务端,然后等待服务端返回模块才可以使用,因此应该采用异步加载。
二、AMD、require.js
AMD规范采用异步的方式加载模块,模块的加载不影响它后面语句的执行,主要用于浏览器。所有依赖这个模块的语句都定义在了回调函数中,等到加载完成后,这个回调函数才会运行。require.js实现了AMD规范,用 require.config() 指定引用路径等,用 define() 定义模块,用 require() 加载模块。
//定义 math.js模块
define(function () {
var num = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
num:num
};
});
//网页中引入文件 require.js 和入口文件 main.js,main.js中配置require.config()
<script src="js/require.js" data-main="js/main"></script>
//入口文件main.js中指定各模块路径和引用名
require.config({
baseUrl:"js/lib",
paths: {
"jquery":"jquery.min", //实际路径 js/lib/jquery.min.js
"math":
}
});
//执行基本操作
require(["jquery","math"], function($,_){
//...
});
三、CMD、Sea.js
CMD规范采用异步的方式加载模块,sea.js实现了CMD规范,CMD与AMD其实很相似,不同点在于:
- AMD推崇依赖前置,提前执行,AMD加载完模块后就会执行该模块,所有模块加载执行完后会进入require的回调函数,执行主逻辑,这样会导致依赖模块的执行顺序和书写顺序可能不一致,但主逻辑一定是在所有依赖加载执行后才会执行。因此说AMD用户体验好。
- CMD推崇依赖就近,延迟执行,CMD加载完某个模块后并不会立即执行,所有模块加载完后进入主逻辑,遇到require语句才执行对应模块,这样模块的执行顺序和书写顺序是一致的。因此说CMD性能好。
- 依赖前置:定义模块的时候要声明其依赖的模块
- 依赖就近:CMD只有在用到某个模块才会require
四、ES6模块
ES6的Module旨在成为浏览器和服务器通用的模块解决方案,它结合了CommonJS和AMD的优点,语法简洁且支持异步加载,用 export 规定模块的对外接口,用 import 用于加载引用模块。
//定义模块 math.js
const num = 0;
function add(a, b) {
return a + b;
}
export{ num, add };
<script type="module" src="./foo.js"></script> //等同于defer属性,渲染完再执行,按顺序加载
<script type="module" src="./foo.js" defer></script>
<script type="module" src="./foo.js" async></script>//打开async属性,下载完就执行,不能保证加载顺序
//加载引用模块
import {num, add} from './math';
function test(x) {
x.textContent = add(99 + num);
}
//整体加载引用模块
import * as obj from './math';
function test(x) {
x.textContent = obj.add(99 + obj.num);
}
ES6的模块不是对象,import命令会在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。
五、CommonJS与ES6模块的不同
1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 一旦输出一个值,模块内部的变化就影响不到这个值。而对于ES6模块,JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
因为 CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
3. CommonJS 模块的require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。
参考资料: