JavaScript 语言诞生至今,模块规范化之路曲曲折折。社区先后出现了各种解决方案,包括 AMD、CMD、CommonJS 等,而后ES6又引入了模块规范。
今天我们就来探究一下,为什么会引入这些模块规范,这些模块规范分别做了什么?

1. ES6之前的模块加载

在ES6原生支持模块前,使用模块的JavaScript代码本质上是希望使用默认没有的语言特性。因此,需要采用特定的模板语法编写代码以及单独的模块工具将模板语法和JavaScript运行时连接起来。

1.1 服务器端 - CommonJS

CommonJS规范概述了同步声明依赖的模块定义,只有加载模块完成,才能执行后面的操作。该规范通常用于在服务器端实现模块化代码。

1.1.1 CommonJS module的加载原理

CommonJS模块是一个脚本文件,当我们用require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成该模块的一个说明对象。

  1. {
  2. id: '', //模块id,唯一
  3. exports: { //模块输出的各个接口
  4. ...
  5. },
  6. loaded: true, //模块的脚本是否执行完毕
  7. ...
  8. }

当我们以后用到该模块时,就会到对象的exports属性中取值,即使我们再次执行**require**命令,也不会再次执行该模块,而是到缓存中取值。同时,因为CommonJs是加载时执行,一旦某个模块被“循环加载”,就只会输出已经执行的部分,没有执行的部分不会输出。
让我们来看看Node.js官方给出的例子:

  1. //a.js
  2. exports.done = false;
  3. var b = require('./b.js');
  4. console.log('在a.js中,b.done = %j', b.done);
  5. exports.done = true;
  6. console.log('a.js执行完毕!')
  7. //b.js
  8. exports.done = false;
  9. var a = require('./a.js');
  10. console.log('在b.js中,a.done = %j', a.done);
  11. exports.done = true;
  12. console.log('b.js执行完毕!')
  13. //main.js
  14. var a = require('./a.js');
  15. var b = require('./b.js');
  16. console.log('在main.js中,a.done = %j, b.done = %j', a.done, b.done);

当我们运行main.js时,运行结果如下:

  1. b.js中,a.done = false
  2. b.js执行完毕!
  3. a.js中,b.done = true
  4. a.js执行完毕!
  5. main.js中,a.done = true, b.done = true

由上述的运行结果可知:

  1. 当我们在首次加载b.js时,a.js并没有执行完毕,只执行了第一行,所以在循环加载中只输出了已执行的部分a.done=false.
  2. main.js第二行不会再次执行,而是输出缓存b.js的执行结果b.done=true.

    1.1.2 modult.exports 和 exports 的区别

    node代码中,有的地方使用module.exports,有的地方使用exports,这两个有什么区别吗?让我们来看看下述的代码: ```javascript var module = { exports: {} } var exports = module.exports; console.log(module.exports === exports); // true

var s = ‘btqf’ exports = s; // module.exports 不受影响 console.log(module.exports === exports); // false

  1. 由运行结果来看,当我们对模块进行初始化时,`exports``module.exports`指向同一块内存,`exports`被重新赋值后,就切断了与原内存地址的联系。所以当一个模块的对外接口为一个单一值,不能使用`exports`输出,而只能使用`module.exports`进行输出。
  2. <a name="S0F2h"></a>
  3. ### 1.2 浏览器端 - AMD/CMD
  4. <a name="lktXi"></a>
  5. #### 1.2.1 AMD
  6. > 异步模块定义规范(AMD)以浏览器为目标执行环境,模块和模块的依赖可以被异步加载(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)
  7. AMD模块实现的核心是用函数包装模块定义,这样可以防止声明全局变量,并允许加载器库控制何时加载代码。同时,包装模块的函数时全局`define`的参数。
  8. ```javascript
  9. // ID为'moduleA'的模块定义, moduleA依赖moduleB
  10. // moduleB会异步加载
  11. define('moduleA', ['moduleB'], function(moduleB) {
  12. return {
  13. stuff: moduleB.doStuff();
  14. }
  15. })

1.2.2 CMD

CMD和AMD一样,都是JS的模块化规范,也主要用于浏览器端。两者的主要区别在于CMD偏向依赖靠近,AMD偏向依赖前置。

  1. // AMD
  2. // 依赖必须一开始就写好
  3. define(['./utils'], function(utils) {
  4. utils.request();
  5. });
  6. // CMD
  7. define(function(require) {
  8. // 依赖可以就近书写
  9. var utils = require('./utils');
  10. utils.request();
  11. });

考虑到目前主流中对AMD和CMD的使用越来越少,大家对AMD和CMD有了大致的认识即可,此处不再过多赘述。

1.3 UMD

为了统一CommonJSAMD生态系统,UMD闪亮登场。UMD定义的模块会在启动时检测要使用哪一个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式中。其解决了JS模块跨模块规范、跨平台使用的问题。

  1. !function (root, factory) {
  2. if (typeof exports === 'object' && typeof module === 'object') {
  3. // CommonJS2
  4. module.exports = factory()
  5. // define.amd 用来判断项目是否应用 require.js
  6. } else if (typeof define === 'function' && define.amd) {
  7. // AMD
  8. define([], factory)
  9. } else if (typeof exports === 'object') {
  10. // CommonJS
  11. exports.myLibName = factory()
  12. } else {
  13. // 全局变量
  14. root.myLibName = factory()
  15. }
  16. }(window, function () {
  17. // 模块初始化要执行的代码
  18. });

2. ES6模块加载

ES6 module旨在为浏览器和服务器提供通用的模块解决方案。但在目前来说,无论在浏览器端还是服务器端,都还没有完全支持ES6 module,如果要想使用可以借助babel等编译器。
ES6 module 在处理以上几种导入模块接口的方式时都是编译时处理,所以importexport命令只能用在模块的顶层。

2.1 导出

  1. // 方式一
  2. export const prefix = 'https://github.com';
  3. // 方式二
  4. const foo = 'btqf';
  5. const bar = 'bar'
  6. export { foo, bar as myBar }
  7. // 方式三: 默认导出
  8. export default function foo() {}

2.2 导入

  1. // 方式一:配合`import`使用的`as`关键字用来为导入的接口重命名。
  2. import { api as myApi } from './config.js';
  3. // 方式二:整体导入
  4. import * as config from './config.js';
  5. const api = config.api;
  6. // 方式三:默认导出的导入
  7. // foo.js
  8. export const conut = 0;
  9. export default function myFoo() {}
  10. // index.js
  11. import { default as cusFoo, count } from './foo.js';
  12. // 方式四:整体加载
  13. import './config.js';
  14. // 方式五:动态加载
  15. function foo() {
  16. import('./config.js')
  17. .then(({ api }) => {});
  18. }

2.3 ES6的循环加载

模块对导出变量,方法,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者保证真正取值时能够取到值,只要引用是存在的,代码就能执行。

  1. //even.js
  2. import {odd} from './odd';
  3. var counter = 0;
  4. export function even(n){
  5. counter ++;
  6. console.log(counter);
  7. return n == 0 || odd(n-1);
  8. }
  9. //odd.js
  10. import {even} from './even.js';
  11. export function odd(n){
  12. return n != 0 && even(n-1);
  13. }
  14. //index.js
  15. import * as m from './even.js';
  16. var x = m.even(5);
  17. console.log(x);
  18. var y = m.even(4);
  19. console.log(y);
  20. // 执行结果
  21. // 1 2 3 false 4 5 6 true

3. CommonJS module 和 ES6 module 的区别

  • **CommonJS**的require语法是同步的,所以就导致了**CommonJS**模块规范只适合用在服务端,而ES6模块无论是在浏览器端还是服务端都是可以使用的,但是在服务端中,还需要遵循一些特殊的规则才能使用 ;
  • **CommonJS** 模块输出的是一个值的拷贝,而ES6 模块输出的是值的引用
  • **CommonJS** 模块是运行时加载,而ES6 模块是编译时输出接口,使得对JS的模块进行静态分析成为了可能
  • 因为两个模块加载机制的不同,所以在对待循环加载的时候,它们会有不同的表现。**CommonJS**遇到循环依赖的时候,只会输出已经执行的部分,后续的输出或者变化,是不会影响已经输出的变量。而ES6模块相反,使用import加载一个变量,变量不会被缓存,真正取值的时候就能取到最终的值;
  • 关于模块顶层的this指向问题,在**CommonJS**顶层,this指向当前模块;而在ES6模块中,this指向undefined;
  • 关于两个模块互相引用的问题,在ES6模块当中,是支持加载**CommonJS**模块的。但是反过来,**CommonJS**并不能requireES6模块,在NodeJS中,两种模块方案是分开处理的。