模块化的目的

在ES6出现之前,JS语言本身并没有提供模块化能力,这为开发带来了一些问题,其中最重要的两个问题应当是全局污染和依赖管理混乱。

<script src="a.js"></script>
<script src="b.js"></script>

例如上述代码引入2个js,但是如果a和b两个js文件中存在同名的全局变量或者方法,就会对接下来的使用造成混乱和错误。因此我们需要引入模块化的规范来减少依赖和混乱。

常见的模块化规范

CommonJS

最大的特征就是module.exports和require。

// a.js
exports = function(x) {
  console.log(x);
};

// b.js
var A = require('a.js');
A("Hello");    // Hello

exports = module.exports

总结来说规范基本是以下4条:

  1. 每个文件都是一个模块;
  2. 在模块内提供module对象,表示当前模块;
  3. 模块使用exports对外暴露自身的函数/对象/变量等;
  4. 模块内通过require()方法导入其他模块;

AMD

对外暴露模块使用了define函数:

// file a.js
 define('moduleName', ['other.js', 'another.js'], function() {
    // code...
 })

可以看到define包含3个参数:

  1. moduleName,该参数可以省略,表示该模块的名字,一般作用不大
  2. ['name1', 'name2'],第二个参数是一个数组,表示当前模块依赖的其他模块,如果没有依赖模块,该参数可以省略
  3. callback,第三个参数是必传参数,是一个回调函数,内部是当前模块的相关代码

另外,AMD规定在运行当前加载模块回调前,会首先将所有依赖包加载完毕,也是就是define函数的第二个参数中指定的依赖包。

CMD

CMD名字与AMD类型,他们的模块定义方式也是类似:

define(function(require, exports, module) {   
    var a = require('./a')  
    a.doSomething();
    // code... 
    var b = require('./b') 
    // code...
})

区别在于依赖的加载机制,AMD是依赖前置,就是在当前模块前就把依赖加载好,CMD则是就近原则,意味着在需要的时候加载依赖。

UMD

又是一个类似名称的叫UMD,它是CommonJS、AMD、CMD的一种兼容体,可以兼容上述3种模式。

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        //Node, CommonJS之类的
        module.exports = factory(require('jquery'));
    } else {
        //浏览器全局变量(root 即 window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
    //方法
    function myFunc(){};
    //暴露公共方法
    return myFunc;
}));

ES6 modules

上面👆的几种模式都是JS社区提出来的,可以说是来自民间贡献,而ES6出来后,官方也带来了JS的模块化功能。

ES6中的模块化能力由两个命令构成:exportimport,export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

// a.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

// b.js
import {firstName, lastName, year} from 'a.js'

console.log(firstName);    // Michael
console.log(lastName);    // Jackson
console.log(year);    // 1958

year = 2019;    // Syntax Error : 'a' is read-only;

在的例子中,可以看到最后我们试图修改引自a.js的year变量会报错,因为这个引入的值是可读的,不可修改。

另外,我们还可以使用 export default 来暴露一个默认的变量,这样的好处是我不需要知道此模块暴露出的变量名/函数名就可以使用。

// a.js

function print(x) {
	console.log(x);
}

export default print;

// b.js
import Print from 'a.js'

Print("Hello");    // Hello

Print 是我自定义的名字,export default 在每个模块中只能执行一次,其本质是导出了一个名为default的变量或函数。