前端工程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 为主,通过构建工具处理兼容性。