模块化方案对比:CommonJS、AMD、ES Module

比较 CommonJS、AMD、CMD 与 ES Module 的加载方式和导出机制

问题

CommonJS、AMD、CMD 与 ES Module 有什么区别?主要从动态/静态加载、值拷贝/引用两个维度分析。

解答

CommonJS

Node.js 采用的模块规范,同步加载,运行时确定依赖。

// math.js
let count = 0;

function add(a, b) {
  count++;
  return a + b;
}

module.exports = {
  count,
  add
};

// main.js
const math = require('./math'); // 同步加载,运行时执行

console.log(math.count); // 0
math.add(1, 2);
console.log(math.count); // 0 —— 值拷贝,不会变化

AMD (Asynchronous Module Definition)

RequireJS 实现的规范,异步加载,适合浏览器环境。

// 定义模块
define('math', [], function() {
  let count = 0;
  return {
    add: function(a, b) {
      count++;
      return a + b;
    },
    getCount: function() {
      return count;
    }
  };
});

// 使用模块
require(['math'], function(math) {
  // 异步回调,依赖前置
  console.log(math.add(1, 2));
});

CMD (Common Module Definition)

SeaJS 实现的规范,异步加载,依赖就近。

// 定义模块
define(function(require, exports, module) {
  // 依赖就近,用到时再 require
  const math = require('./math');
  console.log(math.add(1, 2));
  
  // 条件加载
  if (needOther) {
    const other = require('./other');
  }
});

ES Module

ES6 原生模块系统,静态分析,编译时确定依赖。

// math.js
export let count = 0;

export function add(a, b) {
  count++;
  return a + b;
}

// main.js
import { count, add } from './math.js'; // 静态导入,编译时分析

console.log(count); // 0
add(1, 2);
console.log(count); // 1 —— 引用绑定,实时反映变化

值拷贝 vs 引用绑定

// === CommonJS:值拷贝 ===
// counter.js
let count = 0;
module.exports = {
  count,
  increment() { count++; }
};

// main.js
const counter = require('./counter');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 —— 还是 0,因为是拷贝

// === ES Module:引用绑定 ===
// counter.js
export let count = 0;
export function increment() { count++; }

// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 —— 变成 1,因为是引用

动态加载 vs 静态加载

// CommonJS:动态加载,可以条件引入
if (condition) {
  const moduleA = require('./a');
} else {
  const moduleB = require('./b');
}

// ES Module:静态加载,import 必须在顶层
import { a } from './a.js'; // 必须在顶层,不能在 if 里

// ES Module 动态导入(ES2020)
if (condition) {
  const module = await import('./a.js'); // 返回 Promise
}

对比表格

特性CommonJSAMDCMDES Module
加载方式同步异步异步编译时静态分析
运行环境Node.js浏览器浏览器通用
导出机制值拷贝值拷贝值拷贝引用绑定
依赖声明运行时前置声明就近声明静态声明
Tree Shaking不支持不支持不支持支持

关键点

  • CommonJS 同步加载require 运行时执行,导出值的拷贝,适合服务端
  • AMD 异步前置:依赖前置声明,回调中使用,RequireJS 实现
  • CMD 异步就近:依赖就近书写,用时再加载,SeaJS 实现
  • ES Module 静态分析:编译时确定依赖,导出值的引用绑定,支持 Tree Shaking
  • 值拷贝 vs 引用:CommonJS 导出后修改原值不影响已导入的值;ES Module 导出的是实时绑定,原值变化会同步