📖 核心概念
- 模块化演进:IIFE → CommonJS → AMD → ESM
- ES Modules语法与特性
- CommonJS规范与require机制
- ESM与CJS的差异与互操作
- 动态import()与代码分割
- 模块解析与路径映射
💻 代码实现
ES Modules实战
✅ javascript
// ===== math.js - 纯ESM模块 =====
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export default class Calculator {
#precision = 2;
constructor(precision = 2) { this.#precision = precision; }
round(val) { return Number(val.toFixed(this.#precision)); }
}
// ===== utils.js - 聚合导出 =====
export { add, multiply } from './math.js';
export * as math from './math.js';
// 重命名导出
export { add as sum, multiply as mul } from './math.js';
// ===== app.js - 导入使用 =====
import Calculator, { PI, add } from './math.js';
import { math } from './utils.js';
// 动态导入 - 按需加载
async function loadChart() {
const { Chart } = await import('./chart.js');
return new Chart(document.getElementById('canvas'));
}
// 条件动态导入
if ('IntersectionObserver' in window) {
const { observe } = await import('./lazy-observer.js');
observe();
}
// top-level await (ES2022)
const config = await import('./config.json', {
assert: { type: 'json' }
});
CJS与互操作
✅ javascript
// ===== CommonJS模块 =====
// logger.js
const fs = require('fs');
const path = require('path');
let logLevel = 'info';
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
function log(level, message) {
if (levels[level] >= levels[logLevel]) {
const timestamp = new Date().toISOString();
const line = \`[\${timestamp}] [\${level.toUpperCase()}] \${message}\n\`;
fs.appendFileSync(path.join(__dirname, 'app.log'), line);
}
}
module.exports = { log, setLevel: (l) => { logLevel = l; } };
module.exports.info = (msg) => log('info', msg);
module.exports.error = (msg) => log('error', msg);
// ===== CJS与ESM互操作 =====
// 在ESM中导入CJS模块
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjsModule = require('./legacy-cjs-module');
// 在CJS中动态导入ESM
// commonjs-file.js
async function main() {
const esmModule = await import('./esm-module.js');
esmModule.default.doSomething();
}
// ===== package.json配置 =====
// "type": "module" → .js文件默认ESM
// "type": "commonjs" → .js文件默认CJS
// .mjs → 始终ESM
// .cjs → 始终CJS
// ===== Vite中的模块解析 =====
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@utils': '/src/utils',
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'date-fns'],
},
},
},
},
});
🔍 模块化进阶与实践
循环依赖处理
循环依赖是模块化开发中的常见问题。ESM和CJS对循环依赖的处理方式不同:
循环依赖对比
✅ javascript
// CJS循环依赖:得到未完成模块的引用
// a.js (CJS)
const b = require('./b'); // b此时可能是部分初始化的
exports.value = 'a';
console.log(b.value); // 可能是undefined
// ESM循环依赖:得到引用但值可能未初始化
// a.js (ESM)
import { value as bValue } from './b.js'; // 获取绑定引用
export const value = 'a';
// 此时bValue可能是undefined(TDZ)
// 但在模块完全执行后,bValue会是正确的值
// 解决方案:重构消除循环依赖
// 1. 提取共享逻辑到第三个模块
// 2. 使用事件/回调代替直接调用
// 3. 延迟导入(函数内import)
Tree Shaking原理
Tree Shaking依赖于ESM的静态结构分析。只有使用ESM的命名导出,打包工具才能在编译时确定哪些代码没有被使用:
- 使用命名导出而非默认导出(更好的tree shaking)
- 避免副作用模块(设置package.json的sideEffects字段)
- 使用纯函数而非修改全局状态的模块
- 注意:CJS无法被tree shake
模块联邦(Module Federation)
Webpack 5的Module Federation允许多个独立构建的应用在运行时共享模块,是微前端架构的关键技术:
模块联邦配置
✅ javascript
// 远程应用:暴露组件
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Dashboard': './src/components/Dashboard',
},
shared: ['react', 'react-dom'],
})
// 宿主应用:消费组件
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://remote.example.com/remoteEntry.js',
},
shared: ['react', 'react-dom'],
})
// 在宿主应用中直接使用远程组件
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
🎯 练习任务
- 1 将一个CommonJS项目迁移到ES Modules
- 2 实现一个支持动态import的插件系统
- 3 配置Vite的路径别名和代码分割策略
- 4 对比CJS和ESM在循环依赖场景下的行为差异
🏆 成就解锁
模块化专家 — 理解JavaScript模块体系,构建可维护的代码组织结构