前端工程Technical Deep Dive

前端模块化演进 —— 从 CommonJS 到 ESM 的完整指南

发布时间2026/03/29
分类前端工程
预计阅读2 分钟
作者吴长龙
*

模块化的本质

01.内容

模块化的本质

模块化是将复杂系统拆分为独立、可复用部分的管理方式。JavaScript 模块化经历了漫长的演进过程。

史前时代:全局变量与命名空间

javascript snippetjavascript
// 2000年代的主流写法
var MyApp = MyApp || {};

MyApp.Utils = {
  formatDate: function(date) { /* ... */ },
  ajax: function(url) { /* ... */ }
};

MyApp.Widget = {
  init: function() { /* ... */ }
};

问题

  • 命名冲突
  • 依赖顺序必须手动管理
  • 无法 tree-shaking
  • 难以实现按需加载

CommonJS:服务端的模块系统

2009 年 Node.js 引入 CommonJS:

javascript snippetjavascript
// math.js
module.exports = {
  add: function(a, b) { return a + b; },
  subtract: function(a, b) { return a - b; }
};

// 或者导出单个
exports.add = function(a, b) { return a + b; };

// index.js
const math = require('./math');
console.log(math.add(1, 2));

特点

  • 同步加载
  • 运行时解析
  • 缓存机制(模块只加载一次)
  • 适合服务端

问题:同步加载不适合浏览器。

AMD:异步模块定义

javascript snippetjavascript
// 定义模块
define('myModule', ['dependency'], function(dep) {
  return {
    doSomething: function() { /* ... */ }
  };
});

// 使用模块
require(['myModule'], function(myModule) {
  myModule.doSomething();
});

代表库:RequireJS

优点:异步加载,适合浏览器 缺点:语法繁琐,依赖顺序要求严格

CMD:通用模块定义

Sea.js 推行的方案,结合 CommonJS 和 AMD:

javascript snippetjavascript
define(function(require, exports, module) {
  var dep = require('dependency');
  
  exports.doSomething = function() { /* ... */ };
});

ES Modules:标准化的胜利

ES6 (ES2015) 引入的官方模块系统:

javascript snippetjavascript
// named exports
export function add(a, b) { return a + b; }
export const PI = 3.14;

// default export
export default class Calculator { /* ... */ }

// import
import { add, PI } from './math';
import Calculator from './Calculator';

// 组合导入
import Calculator, { add } from './math';

核心特性

javascript snippetjavascript
// 1. 静态分析(编译时确定依赖)
import { foo } from './module'; // 必须是字符串字面量

// 2. 导入绑定(只读常量)
import { obj } from './module';
obj.prop = 'changed'; // ❌ 报错(严格模式下)
// 注意:这和 CommonJS 不同!
// CommonJS: require 返回的是导出对象的引用,可以修改
// ESM: import 导入的是绑定,不能重新赋值

// 3. 循环引用处理
// a.js
import { b } from './b.js';
export const a = 'a';
export function getB() { return b; }

// b.js
import { a } from './a.js';
export const b = 'b';
export function getA() { return a; }

// 4. 静态结构,天然 tree-shaking
// 打包工具可以分析 import 语句,只打包用到的代码

实际应用:混合 CommonJS 和 ESM

现代项目通常两者混用:

javascript snippetjavascript
// package.json
{
  "type": "module",  // 启用 ESM
  "exports": {
    ".": {
      "import": "./dist/index.mjs",  // ESM
      "require": "./dist/index.cjs"  // CommonJS
    }
  }
}

Node.js 中的处理

javascript snippetjavascript
// ESM (.mjs 或 "type": "module")
import fs from 'fs';  // 动态绑定
import { readFile } from 'fs/promises';

// CommonJS (.cjs 或无后缀)
const fs = require('fs');

// 互操作
import cjsModule from './commonjs-module.cjs';  // 可以
import { namedExport } from './commonjs-module.cjs';  // 不行

模块解析策略

#### 1. 相对/绝对路径

javascript snippetjavascript
import utils from './utils/index.js';
import config from '/config/app.js';  // 绝对路径

#### 2. 节点模块

javascript snippetjavascript
import lodash from 'lodash';
import React from 'react';

#### 3. 路径别名

javascript snippetjavascript
// webpack.config.js
resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src'),
    '@components': path.resolve(__dirname, 'src/components')
  }
}

// 使用
import Button from '@/components/Button';

动态导入

javascript snippetjavascript
// 静态导入
import _ from 'lodash'; // 立即加载

// 动态导入 - 代码分割
const _ = await import('lodash'); // 按需加载

// 条件加载
if (isAdmin) {
  const AdminPanel = await import('./AdminPanel');
}

循环依赖的最佳实践

javascript snippetjavascript
// a.js
import { b } from './b.js';
export const a = 'a';

// b.js
import { a } from './a.js';
export const b = a + 'b'; // 可能是 undefined!

// 更好的设计:避免循环依赖
// 1. 提取共享模块
// 2. 延迟导入(函数内 require)
// 3. 重新设计架构

性能考量

#### 1. HTTP/2 多路复用

HTTP/2 下,多个小文件不再是问题:

javascript snippetjavascript
// HTTP/1: 6个并发连接限制
import './a.js';
import './b.js';
import './c.js';
// ... 需要合并

// HTTP/2: 无限制并发
import './a.js';
import './b.js';
import './c.js';
// 并行加载

#### 2. 代码分割

javascript snippetjavascript
// 路由级分割
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

// 组件级分割
const HeavyChart = lazy(() => import('./HeavyChart'));

#### 3. 预加载

javascript snippetjavascript
<link rel="modulepreload" href="./utils.js">

总结

方案加载方式适用场景特点
CommonJS同步Node.js简单、运行时
AMD异步浏览器(早期)回调地狱
CMD延迟浏览器兼容性好
ESM静态浏览器/Node标准、Tree-shaking
UMD兼容库作者全平台

现代前端推荐:ESM 为主,通过构建工具处理兼容性。