一文弄清CommonJS、CMD、ESM等主流模块化规范!

198 阅读7分钟

入行前端以来,总是能在一些技术文章中看到CommonJSAMDCMDUMDESM等字眼,只知道它们都是模块化规范,但具体是什么,就说不上来。

这种感觉太难受了,今天,就详细梳理一下它们的相关知识,非要把它们整明白不可。

模块化概述

随着前端开发越来越复杂,我们传统的开发模式,尤其是团队合作中,非常容易出现命名冲突、文件依赖繁琐的问题。

为解决这些问题,模块化应用而生。

所谓模块化,就是将一个复杂的整体,按照一定的规范,封装成几个模块(文件),每个模块负责单一的职能,且相互隔离,但可以通过特定的接口与其他模块通信。

模块化的出现,方便了代码复用,方便了团队合作与后期维护,大大提升了开发效率。

主流模块化规范

随着前端发展对模块需求越来越大,社区中逐渐出现了一些优秀且被大多数人认同的模块化解决方案,慢慢演变成了通用的模块化规范,主要包括CommonJSAMDCMDUMDESM这五种。

CommonJS

同步模块加载机制,只有所有模块全部加载完成,才能执行后面的操作,主要用于服务器端,若在浏览器端,则模块需要提前编译打包处理。

一、特点

CommonJS 模块输出的是值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。而且,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,返回的都是第一次运行结果的缓存,除非手动清除系统缓存。。

二、代表

1、Node 是 CommonJS 在服务器端一个具有代表性的实现。(由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式)

2、Browserify 是 CommonJS 在浏览器中的一种实现。

3、webpack 打包工具对 CommonJS 的支持和转换。

三、用法

1、暴露模块

module.exports = value 
exports.xxx = value

2、引入模块

如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

require(xxx)

AMD

异步模块加载机制。AMD是专门为浏览器环境设计的,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器会处于”假死”状态。因此,浏览器端的模块,不能采用”同步加载”,只能采用”异步加载”,这就是AMD规范诞生的背景。

一、特点

允许非同步加载模块,也可以根据需要动态加载模块。

二、代表

require.js

三、用法

1、暴露模块

(1)定义一个没有依赖的模块,并向外暴露

// module1.js
define(function(){
    const name = '张三';
    function getName(){
        return name;
    }
    // 暴露模块
    return {name, getName};
});

(2)定义一个有依赖的模块,并向外暴露

// module1.js 将依赖模块作为实参传递进去
define(['module1'],function(module1){
    const age = 20;
    function showName(){
        return module1.name;
    }
    return {age, showName};
});

2、引入模块

//在main.js中引入模块
(function(){
    require.config({
        // 以相对目录的方式指定一个基本路径
        baseUrl:'./js/',
        paths:{
            // 以基本路径开始的相对路径
            module1:'./module/module1',
            module2:'./module/module2'
        }
    });
    require(['module2'],function(module2){
    //会将模块当作参数传进来
        console.log(module2.age); // 20
        console.log(module2.showName());  // 张三
    });
})()
​

CMD

它汲取了 CommonJSAMD 规范的优点,也是专门用于浏览器的异步模块加载。

一、特点

模块的加载是异步的,模块使用时才会加载执行。

二、代表

Sea.js

三、用法

使用全局函数define定义模块,使用export暴露模块,使用require引入模块

1、暴露模块

(1) 定义一个没有依赖的模块

定义模块使用define函数,传入一个函数,函数参数为require,exports,module,require参数用来引入模块,后面两个参数用来暴露模块,暴露模块的方法和Commonjs规范一样,可以使用module.exports或者exports

// module1.js
define(function(require,exports,module){
    let name = '张三';
    function getName(){
        return name;
    }
    module.exports = {name, getName};
 });

(2) 定义一个有依赖的模块

  • 使用同步引入模块,同步可能导致堵塞
// module2.js
 define(function(require,exports,module){
     let module1 = require('./module1.js');
     let age = 20;
     console.log(module1.name);
     console.log(module1.getName());
     module.exports = {age};
 })
  • 使用异步引入模块
// module2.js
define(function(require,exports,module){
     let age = 20;
    // 引入模块暴露的对象作为实参传入m1形参中
    require.async('./module1.js',function(m1){
        console.log(m1.name);
        console.log(m1.getName());
    });
    module.exports = {age};
 })

2、引入模块

引入模块可以使用同步引入require()和异步引入require.async()

// main.js同步引入模块
define(function(require,exports,module){
    let m2 = require('./module/module2.js');
    console.log(m2.age);
})

UMD

UMDAMDCMDCommonJS的糅合,多被一些需要同时需要支持浏览器端、服务端引用的项目所使用的。

一、特点

UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

二、用法

(function (window, factory) {
    if (typeof exports === 'object') {
    
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
    
        define(factory);
    } else {
    
        window.eventUtil = factory();
    }
})(thisfunction () {
    //to do
});

可以看到,define 是 AMD 语法,而 exports 只在 CommonJS 中存在,你会发现它在定义模块的时候会检测当前使用环境和模块的定义方式,如果匹配就使用其规范语法,全部不匹配则挂载在全局对象上,我们看到传入的是一个 this ,它在浏览器中指的就是 window ,在服务端环境中指的就是 global ,使用这样的方式将各种模块化定义都兼容。

ESM

ES标准的模块化规范,从 ES6 开始,JavaScript 才真正意义上有自己的模块化规范(现在主流框架用的都是ESM规范)。ES6 Module默认目前还没有被浏览器支持,需要使用babel。

一、特点

设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用。等到脚本真正执行的时候,才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。ES6模块是动态引入的,并不会缓存值,模块里变量总是绑定其所在的模块。

二、用法

ES6模块使用import关键字导入模块,export关键字导出模块

1、暴露模块

(1)正常导出

const name = '张三'
const age = 24
// 一起导出
export {
  name,
  age
}
// 单个导出
export getName = function() {
  return name
}

(2) 默认导出

const sex = '男'
export default sex

const sex = '男';
export default { sex as e }//设置别名
  • export default anything ,anything 可以是函数,属性方法,或者对象。
  • 对于引入默认导出的模块,import anyName from 'module', anyName 可以是自定义名称。

2、引入模块

import sex, { name, age, getName} from './moudule.js' // sex 是模块默认导出的,{}里是模块正常导出的

import { } 内部的变量名称,要与 export { } 完全匹配

模块化规范小结

1、CommonJS规范主要用于服务端编程,加载模块是同步的,它不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。

2、AMD CMD规范主要服务于浏览器端,两者都是异步加载,只是执行时机不一样。AMD是依赖前置,提前执行,CMD是依赖就近,延迟执行。

3、UMD 集合了CommonJsCMDAMD规范于一身,它可以让我们在合适的环境选择合适的模块规范。

4、ESM 在语言标准的层面上,实现了模块异步加载,可以取代 CommonJS AMD 规范,成为浏览器和服务器通用的模块解决方案。且它是编译时加载的,而CommonJS``AMD CMD都是运行时加载。

image.png