📚 现代前端开发 目录
阶段2:工程化
第 8 / 35 课

模块化:ESM与CommonJS

代码组织的基石

📖 核心概念

💻 代码实现

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的命名导出,打包工具才能在编译时确定哪些代码没有被使用:

模块联邦(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'));

🎯 练习任务

🏆 成就解锁
模块化专家 — 理解JavaScript模块体系,构建可维护的代码组织结构