随着js模块化思想的诞生与变迁,社区逐渐形成了越来越多的模块加载规范,常见的如CommonJS、AMD、CMD、CommonJS等,他们有什么区别?分别是如何使用的呢? 下面用一些例子来分别介绍一下
CommonJS
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志”Javascript模块化编程”正式诞生,因此模块加载规范也随之诞生而来。CommonJS的核心思想是允许模块通过 require
方法来同步加载所要依赖的其他模块,然后通过 exports
或 module.exports
来导出需要暴露的接口,所有代码都运行在模块作用域,不会污染全局作用域。
//a.js
function test1() {
console.log('test1');
}
function test2() {
console.log('test2');
}
exports.test2 = test2;
module.exports = {
test1
}
//index.js
const f = require('./a.js');
f.test1(); // test1
f.test2(); // error: f.test2 is not a function
值得注意的是,module.exports
和exports
在模块声明时,最初都指向同一个引用地址,即exports = module.exports = {}
,但导出的对象为 module.exports
。因此,需要注意尽量不要重新声明module.exports
和exports
,以免断开两者之间的关联,导致意料之外的错误。
CommonJS规范使用require加载模块是同步的,只有加载完成后才能进行后面的操作,不仅加载速度慢,而且还可能导致性能、可用性、调试和跨域访问等问题。nodejs主要应用于服务器编程,加载的模块一般存在于本地硬盘中,加载起来比较快,但是同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的。因此在浏览器端,requirejs和seajs之类的工具包也出现了,他们分别在推广过程中诞生了对应的模块加载规范,即AMD和CMD
AMD- 异步模块定义规范
AMD是RequireJS在推广过程中对模块定义的规范化产出,他可以异步加载依赖模块,并且会提前加载。模块必须采用特定的define()
函数来定义,主要接口为define(id?, dependencies?, factory)
,demo如下:
// module1.js module1 不依赖其他模块 可以直接define(factory) 导出add
define((require, exports, module) => {
const add = (x, y) => x + y;
return { add };
});
// 也可以使用exports或者module.exports来导出 不过优先级return > module.exports > exports
define((require, exports, module) => {
const add = (x, y) => x + y;
exports.count = 2
exports.add = add
module.exports = { add }
});
// 如果return是唯一代码,还可以简写为object
define({
count: 2,
add:(x, y) => x + y,
});
// module2.js module2引入module中的add函数 导出add3
define(['module1'], function ({ add }) {
const add3 = n => add(3, n);
return { add3 };
});
// module3.js
define(['module1'], function ({ add }) {
const add6 = n => add(6, n);
return { add6 };
});
<!--index.html 注意,需要先引入requirejs,定义全局变量require和define-->
<script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"></script>
<script>
// module1为路径,也可写为 define(['./module1'],cb)
// 推荐使用require全局配置来定义每个模块的加载路径,可以是绝对路径,也可以是相对路径
require.config({
paths: {
module1: './module1',
module2: './module2'
},
});
</script>
<script>
require(['module2', 'module3'], ({ add3 }, { add6 }) => {
console.log(add3(4)); // 7
console.log(add6(2)); // 8
});
</script>
CMD- 通用模块定义规范
CMD是SeaJS在推广过程中对模块定义的规范化产出。他和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。。AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。AMD要求依赖前置,在最开始就提前加载所需要的模块,而CMD推崇依赖就近,可以在任何地方去加载
<!-- index.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/seajs/3.0.3/runtime.js"></script>
<script>
seajs.config({
base: './',
alias: {
module1: 'module1.js',
module2: 'module2.js',
module3: 'module3.js',
},
});
seajs.use('./index.js');
</script>
// index.js
define(function (require, exports, module) {
const module3 = require('./module3.js');
module3.add6(5); // 11
require.async('./module2', function (module2) {
module2.add3(3); // 6
});
});
ES6模块
ECMAScript6 标准增加了 JavaScript 语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"。普通示例如下:
// a.js
// 使用export命令暴露模块的对变量
export const name2 = 'name2';
export function getName2() {
console.log('getName2');
}
function getName() {
console.log('getName');
}
const name = 'name';
// export后跟大括号指定所要输出的一组变量与上面单独输出是等价的
// 但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量
export { name, getName };
// index.js
// 通过import加载这个模块
import { name, name2, getName, getName2, } from './a';
es6 module中还声明了一个as
关键字可以用来对变量进行重命名
// a.js
const name3 = 'name3';
export { name1, name3 as name4 };
// 对输出的变量name3 重命名为 name4 等价于
export const name4 = name3;
// index.js
import { name, getName, name4 as name3 } from './a';
console.log('name3', name3); // name3
es6 module中还可以使用export default
命令,为模块指定默认输出。
// a.js
export default function fn1() {
console.log('fn1');
}
// index.js
import f from './a'
f(); // fn1
本质上,export default就是输出一个叫做default的变量或方法
// a.js
// 输出一个叫做default的变量,所以它后面不能跟变量声明语句
export default const name = 'name2' // error
export default name = 'name'
// index.js
import a from './a'
console.log(a); // name
// 同理,我们可以用as进行重命名
import { default as a } from './a'
console.log(a); // name
当需要导出的内容在不同文件时,我们可以单独做一个汇总:
// util1.js
export default fn1
export const fn11 = fn11
// util2.js
export default fn2
// util3.js
export { fn3, fn4, fn5 }
// index.js
export { default as fn1, fn11 } from './util1';
export { default as fn2 } from './util2';
export { fn3, fn4, fn5 } from './util3';
// 这样 可以直接从index引入fn1,fn2,fn3...
import { fn1, fn2, fn3, fn4} from './index'
举例一些常见的错误使用方式
// a.js
export function fn1(){}
// index.js
import a from './a'
a.fn1() // Uncaught TypeError: Cannot read property 'fn1' of undefined
// `import moduleName from 'module'`moduleName指的是模块module中export default 导出的内容,若未导入则为undefined。正确方式为应该为
import { fn1 } from './a'
fn1();
// a.js
export default {
fn1: function () {},
};
// index.js
import { fn1 } from './a'
fn1() // import error: 'fn1' is not exported from './a'.
// {}内的内容是export导出的所有变量,而fn1是default对象下的key
// 正确使用方式为:
import A from './a' // 引入方式1
import { default as A } from './a'; // 引入方式2 两者等价
A.fn1()
// a.js
export const name = '2';
export default {
fn1: function () {},
};
// index.js
import * as A from './a';
A.fn1(); // import error: 'fn1' is not exported from './a'
// * 包含export导出的所有内容,包含name、default等
// 正确使用方式为:
A.default.fn1();
总结
下面用表格来分别概括他们
|规范 | 特点 | api | 代表 |
---|---|---|---|
CommonJS | 同步加载 | module.exports/exports、require | nodejs |
AMD | 异步加载、依赖前置 | define、require | RequireJS |
CMD | 异步加载、依赖就近 | define、require.async | SeaJS |
es6 module | 静态加载,编译时进行静态分析 | export、import | es6 |
参考链接: