模块化演进

What — 是什么

JavaScript 模块化从无到有经历了全局函数 → 命名空间 → IIFE → CommonJS → AMD/CMD → UMD → ES Module 的完整演进,ES Module(ESM)是最终的官方标准。

模块化演进时间线

阶段年份代表方案核心机制解决的问题
全局函数1995函数挂载到 window代码复用(最原始)
命名空间~2005YUI、Dojo对象挂载到 window减少全局污染
IIFE~2008jQuery 插件闭包隔离作用域真正的私有变量
CommonJS2009Node.jsrequire/exports 同步加载服务端模块化
AMD2009RequireJSdefine/require 异步加载浏览器端模块化
CMD2011SeaJSdefine/require 按需加载浏览器端模块化(国内)
UMD~2012通用模板兼容 CJS + AMD + 全局跨环境通用模块
ES Module2015ES6 标准import/export 静态分析官方标准,全平台统一

各阶段详解

1. 全局函数时代

最原始的方式:将不同功能的函数直接定义在全局作用域。

// 全局函数 — 严重污染全局命名空间
function formatData(data) { /* ... */ }
function validateForm(form) { /* ... */ }
function sendMessage(msg) { /* ... */ }

// 问题:命名冲突、无法私有化、依赖关系不明确
// 如果两个库都定义了 validate() 函数,后者覆盖前者

2. 命名空间模式

用对象包裹,减少全局变量数量,但无法真正隔离私有成员。

// 命名空间模式 — 减少了全局变量,但没有私有性
var MyApp = MyApp || {};

MyApp.Utils = {
  formatData: function(data) { /* ... */ },
  parseJSON: function(str) { /* ... */ }
};

MyApp.Form = {
  validate: function(form) { /* ... */ },
  submit: function(form) { /* ... */ }
};

// 问题:MyApp.Utils.formatData 仍然可以被外部直接修改
// MyApp.Utils.formatData = null; // 所有使用方立刻崩溃
// 内部状态完全暴露,没有封装

3. IIFE(立即执行函数表达式)

利用闭包创建私有作用域,是第一种真正实现封装的模块化方案。

// IIFE 模块模式 — 闭包隔离,暴露公共接口
var UserModule = (function() {
  // 私有变量和函数(外部无法访问)
  var privateList = [];
  var privateId = 0;

  function generateId() {
    return ++privateId;
  }

  // 返回公共接口
  return {
    add: function(name) {
      privateList.push({ id: generateId(), name: name });
    },
    getList: function() {
      return privateList.slice(); // 返回副本,防止外部修改
    }
  };
})();

UserModule.add('Alice');
console.log(UserModule.getList()); // [{ id: 1, name: 'Alice' }]
console.log(UserModule.privateList); // undefined(私有不可访问)
console.log(privateId); // ReferenceError(私有不可访问)

IIFE 还可以接收依赖参数:

// IIFE 依赖注入模式
var MyModule = (function($, _) {
  // $ 是 jQuery,_ 是 lodash
  function processItems(items) {
    return _.map(items, function(item) {
      return $(item).text();
    });
  }
  return { processItems: processItems };
})(jQuery, _);

4. CommonJS(CJS)

Node.js 采用的模块规范,同步加载,适合服务端文件系统。

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

// 或具名导出
exports.multiply = function(a, b) { return a * b; };

// app.js — 导入
const math = require('./math');
const { add } = require('./math');

console.log(math.add(1, 2)); // 3
console.log(add(3, 4));       // 7

5. AMD(Asynchronous Module Definition)

RequireJS 提出的浏览器端异步模块规范,回调方式加载。

// AMD 模块定义
define('mathModule', ['dependency1', 'dependency2'], function(dep1, dep2) {
  // 模块体
  function add(a, b) { return a + b; }

  return {
    add: add
  };
});

// AMD 模块加载
require(['mathModule'], function(math) {
  console.log(math.add(1, 2)); // 3
});

6. CMD(Common Module Definition)

SeaJS 提出的规范,与 AMD 的核心区别:推崇依赖就近、按需加载。

// CMD 模块定义
define(function(require, exports, module) {
  // 依赖就近书写,用到时才 require
  var dep1 = require('./dep1');

  function add(a, b) {
    var dep2 = require('./dep2'); // 按需引入
    return dep2.process(a + b);
  }

  module.exports = { add: add };
});

CMD vs AMD 对比:

维度AMD(RequireJS)CMD(SeaJS)
依赖声明前置(定义时声明所有依赖)就近(使用时才 require)
执行时机依赖全部加载后立即执行依赖加载后延迟执行(按需)
推崇者国际社区国内(玉伯)
现状已淘汰已淘汰
代表库RequireJSSeaJS

7. UMD(Universal Module Definition)

通用模块定义,让同一段代码兼容 CJS、AMD 和全局变量三种环境。

// UMD 包装器 — 兼容 CJS + AMD + 全局变量
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD 环境
    define(['dependency'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS 环境
    module.exports = factory(require('dependency'));
  } else {
    // 浏览器全局变量
    root.MyLibrary = factory(root.dependency);
  }
})(typeof self !== 'undefined' ? self : this, function(dependency) {
  // 模块实际逻辑
  function myMethod() {
    return dependency.doSomething();
  }

  return {
    myMethod: myMethod
  };
});

8. ES Module(ESM)

ECMAScript 2015 引入的官方模块标准,静态分析、全平台支持。

// 命名导出
export const PI = 3.14159;
export function circleArea(r) { return PI * r * r; }
export default class Shape { /* ... */ }

// 命名导入
import Shape, { PI, circleArea } from './math.js';

// 命名空间导入
import * as MathModule from './math.js';

// 重命名导入
import { circleArea as area } from './math.js';

ESM 详细特性

1. 静态分析(Static Structure)

ESM 的 import/export 语句必须出现在模块顶层,不能放在条件语句或函数中,使得打包工具能在编译时确定依赖关系。

// ESM 静态导入 — 编译时确定
import { utils } from './utils.js'; // 必须顶层,路径必须是字符串字面量

// 以下写法都是非法的:
// if (condition) { import { foo } from './bar.js'; } // SyntaxError
// const path = './' + name + '.js'; import { foo } from path; // SyntaxError

// 动态导入需要用 import() 函数
if (condition) {
  const module = await import('./heavy-module.js'); // 合法
}

2. import.meta

ESM 模块的元信息对象,最常用的属性是 url(当前模块的 URL)。

// browser.js — 浏览器环境
console.log(import.meta.url); // "https://example.com/module.js"

// Node.js 中替代 __dirname
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__filename); // "/project/src/module.js"
console.log(__dirname);  // "/project/src"

3. 顶层 await(Top-Level Await)

ES2022 特性,允许在模块顶层直接使用 await,无需包裹在 async 函数中。

// config.js — 顶层 await 模块
const response = await fetch('/api/config');
const config = await response.json();

export default config;

// 其他模块导入此模块时会等待顶层 await 完成
import config from './config.js';
console.log(config); // 已加载完成的配置

[!warning] 顶层 await 限制 顶层 await 只能在 ESM 模块中使用。使用顶层 await 的模块会变成”异步模块”,导入它的模块也会被隐式变为异步,可能影响加载性能。

Why — 为什么

模块化解决的核心问题

1. 命名冲突

// 无模块化:全局变量冲突
// libA.js
var name = 'Library A';
function init() { console.log(name); }

// libB.js
var name = 'Library B'; // 覆盖了 libA 的 name
function init() { console.log(name); } // 覆盖了 libA 的 init

// 模块化后:各自作用域隔离
// libA.js
export const name = 'Library A';
export function init() { console.log(name); }

// libB.js
export const name = 'Library B'; // 不冲突,不同模块
export function init() { console.log(name); }

2. 依赖管理

// 无模块化:依赖靠 script 标签顺序,难以维护
// <script src="jquery.js"></script>
// <script src="lodash.js"></script>
// <script src="utils.js"></script>  <!-- 依赖 jquery 和 lodash -->
// <script src="app.js"></script>    <!-- 依赖 utils -->
// 顺序错了就报错,而且无法在代码中看出依赖关系

// 模块化后:依赖显式声明
import $ from 'jquery';
import _ from 'lodash';
import { formatDate } from './utils.js';

3. 代码复用

// 模块化后,一段逻辑可以跨项目复用
// shared/validators.js
export function isEmail(str) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}

export function isPhone(str) {
  return /^1[3-9]\d{9}$/.test(str);
}

// 项目A、项目B 都可以直接 import 使用

对比方案(完整版)

维度ES ModuleCommonJSAMDCMDUMDIIFE
加载方式静态(编译时)动态(运行时同步)动态(异步)动态(按需异步)取决于环境即时执行
Tree Shaking支持不支持不支持不支持不支持不支持
浏览器支持原生支持不支持需库需库需适配原生
Node.js 支持14+ 稳定原生不支持不支持可用可用
循环依赖引用绑定(部分可用)值拷贝(可能 undefined)不支持不支持取决于环境无模块依赖
异步加载import()不支持回调回调不支持不支持
私有变量模块作用域模块作用域函数作用域函数作用域取决于环境闭包
标准化ECMA 官方社区规范社区规范社区规范社区模板
现状推荐使用仍在使用已淘汰已淘汰旧包使用特定场景

优缺点

  • ESM 优点:
    • 静态分析,支持 Tree Shaking
    • 浏览器原生支持
    • 顶层 await 支持
    • 导出是 live binding
    • ECMA 官方标准,长期方向
    • import() 支持动态加载
  • ESM 缺点:
    • 部分 npm 包只有 CJS 版本
    • Node.js ESM 兼容仍有一些边界问题
    • 动态条件导入不如 CJS 灵活
    • 顶层 await 可能导致模块加载链变慢
    • 浏览器中 import 路径必须包含完整扩展名

How — 怎么用

快速上手

// ESM 导出
export const name = 'Alice';
export function greet() { return `Hello ${name}`; }
export default class User { /* ... */ }

// ESM 导入
import User, { name, greet } from './user.js';
import * as UserModule from './user.js';

// 动态导入
const module = await import('./heavy-module.js');

代码示例

示例 1:IIFE 模块模式完整示例

// iife-event-bus.js — 用 IIFE 实现发布订阅模式
var EventBus = (function() {
  var events = {}; // 私有:事件监听器存储

  function on(event, callback) {
    if (!events[event]) events[event] = [];
    events[event].push(callback);
  }

  function off(event, callback) {
    if (!events[event]) return;
    events[event] = events[event].filter(function(cb) {
      return cb !== callback;
    });
  }

  function emit(event, data) {
    if (!events[event]) return;
    events[event].forEach(function(cb) {
      cb(data);
    });
  }

  // 只暴露公共方法
  return { on: on, off: off, emit: emit };
})();

// 使用
EventBus.on('user:login', function(user) {
  console.log('Welcome, ' + user.name);
});
EventBus.emit('user:login', { name: 'Alice' });

示例 2:命名空间模式示例

// namespace-pattern.js — 命名空间模式 + 子模块
var App = App || {};

// 核心模块
App.Core = (function() {
  var version = '1.0.0';
  return {
    getVersion: function() { return version; }
  };
})();

// 工具模块
App.Utils = {
  debounce: function(fn, delay) {
    var timer = null;
    return function() {
      var context = this, args = arguments;
      clearTimeout(timer);
      timer = setTimeout(function() {
        fn.apply(context, args);
      }, delay);
    };
  },
  throttle: function(fn, interval) {
    var lastTime = 0;
    return function() {
      var now = Date.now();
      if (now - lastTime >= interval) {
        lastTime = now;
        fn.apply(this, arguments);
      }
    };
  }
};

// 模块间通过命名空间通信
App.Core.getVersion(); // "1.0.0"
App.Utils.debounce(function() {}, 300);

示例 3:UMD 包装器写法

// umd-library.js — 发布 npm 包时常用的 UMD 模板
(function(global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? factory(exports)                              // CommonJS
    : typeof define === 'function' && define.amd
      ? define(['exports'], factory)                // AMD
      : (global = typeof globalThis !== 'undefined'
          ? globalThis : global || self,
        factory(global.MyLib = {}));                // 浏览器全局
})(this, function(exports) {
  'use strict';

  // 模块逻辑
  function create(config) {
    return { config: config, created: true };
  }

  function validate(input) {
    return typeof input === 'string' && input.length > 0;
  }

  // 导出
  exports.create = create;
  exports.validate = validate;
  Object.defineProperty(exports, '__esModule', { value: true });
});

示例 4:AMD define/require 完整示例

// amd-cart.js — AMD 模块定义(RequireJS 语法)
define('Cart', ['jquery', 'lodash', './storage'], function($, _, Storage) {
  var items = [];

  function addItem(product) {
    var existing = _.find(items, { id: product.id });
    if (existing) {
      existing.quantity += 1;
    } else {
      items.push({ ...product, quantity: 1 });
    }
    Storage.save('cart', items);
  }

  function getTotal() {
    return _.reduce(items, function(sum, item) {
      return sum + item.price * item.quantity;
    }, 0);
  }

  function render() {
    $('#cart-list').html(
      items.map(function(item) {
        return '<li>' + item.name + ' x' + item.quantity + '</li>';
      }).join('')
    );
  }

  return {
    addItem: addItem,
    getTotal: getTotal,
    render: render
  };
});

// main.js — AMD 模块加载
require(['Cart'], function(Cart) {
  Cart.addItem({ id: 1, name: 'Book', price: 29.9 });
  Cart.addItem({ id: 2, name: 'Pen', price: 5.0 });
  Cart.render();
  console.log('Total:', Cart.getTotal());
});

示例 5:ESM live binding 演示

// 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(ESM live binding,值已更新)

// 对比 CommonJS:
// const { count, increment } = require('./counter');
// console.log(count); // 0(值拷贝,永远是初始值)

示例 6:动态 import() 与代码分割实战

// 路由级代码分割
async function loadPage(route) {
  switch (route) {
    case '/dashboard':
      const { DashboardPage } = await import('./pages/dashboard.js');
      return DashboardPage;
    case '/settings':
      const { SettingsPage } = await import('./pages/settings.js');
      return SettingsPage;
    case '/profile':
      const { ProfilePage } = await import('./pages/profile.js');
      return ProfilePage;
    default:
      const { NotFound } = await import('./pages/not-found.js');
      return NotFound;
  }
}

// 条件加载重型依赖
async function initEditor() {
  if (!document.querySelector('#editor')) return;

  // 仅在需要时加载 Monaco Editor(约 2MB)
  const loader = await import('monaco-editor');
  const editor = loader.create(document.querySelector('#editor'), {
    language: 'javascript',
    theme: 'vs-dark'
  });
  return editor;
}

// 重试机制包装动态导入
async function importWithRetry(specifier, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await import(specifier);
    } catch (err) {
      if (i === retries - 1) throw err;
      console.warn(`Import failed, retrying (${i + 1}/${retries})...`);
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

示例 7:Tree Shaking 配置与验证

// utils.js — 只有 add 和 multiply 会被 Tree Shaking 保留
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// 这个函数如果没被 import,会被 Tree Shaking 移除
export function unusedHeavyFunction() {
  // 大量计算逻辑...
  return new Array(10000).fill(0).map((_, i) => Math.sqrt(i));
}

// main.js — 只导入了 add
import { add } from './utils.js';
console.log(add(1, 2));
// webpack.config.js — Tree Shaking 配置
module.exports = {
  mode: 'production', // 生产模式自动启用 Tree Shaking
  optimization: {
    usedExports: true,    // 标记未使用的导出
    minimize: true,       // 压缩代码(移除标记的未使用代码)
    sideEffects: true     // 读取 package.json 的 sideEffects 字段
  }
};
// package.json — 声明无副作用,允许更激进的 Tree Shaking
{
  "name": "my-utils",
  "sideEffects": false,
  "sideEffects": ["*.css", "./src/polyfill.js"]
}
// 验证 Tree Shaking 是否生效
// 构建后搜索 unusedHeavyFunction,如果没有出现则说明生效

// 注意:以下写法会导致 Tree Shaking 失效
import { add } from './utils';      // 命名导入 ✅ 可 Shaking
import _ from 'lodash';             // 默认导入整个库 ❌ 无法 Shaking
import * as Utils from './utils';   // 命名空间导入 ❌ 无法 Shaking
const { add } = require('./utils'); // CJS ❌ 无法 Shaking

示例 8:ESM 顶层 await 用法

// db-connection.js — 数据库连接模块(顶层 await)
import { MongoClient } from 'mongodb';

const client = new MongoClient('mongodb://localhost:27017');
await client.connect(); // 顶层 await:连接数据库

const db = client.db('myapp');
export { db, client };

// app.js — 导入时自动等待连接完成
import { db } from './db-connection.js';

// 此时 db 已连接就绪,可直接使用
const users = await db.collection('users').find({}).toArray();
console.log(users);
// config-loader.js — 配置加载模块
const configPath = process.env.CONFIG_URL || '/api/config';
const response = await fetch(configPath);
if (!response.ok) {
  throw new Error(`Failed to load config: ${response.status}`);
}
const config = await response.json();

// 根据配置决定导出内容
export const apiUrl = config.apiUrl;
export const features = config.features;
export default config;
// 注意:顶层 await 会让模块变为"异步模块"
// 导入它的模块也会等待,可能形成瀑布式加载

// ❌ 反模式:多个串行顶层 await
// a.js
export const dataA = await fetch('/api/a').then(r => r.json());

// b.js(依赖 a.js)
import { dataA } from './a.js'; // 等待 a.js 的 fetch 完成
export const dataB = await fetch('/api/b').then(r => r.json()); // 再 fetch b

// ✅ 更好:使用 Promise.all 并行
const [dataA, dataB] = await Promise.all([
  fetch('/api/a').then(r => r.json()),
  fetch('/api/b').then(r => r.json())
]);
export { dataA, dataB };

示例 9:Dual Package(同时支持 ESM 和 CJS)完整配置

// package.json — 双格式发布配置
{
  "name": "my-awesome-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    },
    "./utils": {
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  },
  "files": ["dist"],
  "sideEffects": false
}
// src/index.ts — 源码(TypeScript)
export function greet(name: string): string {
  return `Hello, ${name}!`;
}

export const version = '1.0.0';
// tsconfig.json — 生成双格式输出
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "outDir": "./dist"
  }
}
// build.js — 构建脚本(使用 esbuild 生成双格式)
import { build } from 'esbuild';

// ESM 版本
await build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  format: 'esm',
  outfile: 'dist/index.mjs',
  platform: 'neutral'
});

// CJS 版本
await build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  format: 'cjs',
  outfile: 'dist/index.cjs',
  platform: 'neutral'
});
// package.json — 使用条件导出时的"双包风险"规避
// 问题:同一个应用中如果通过 CJS 和 ESM 分别引入同一包,
// 会得到两个不同的模块实例(单例失效)

// 解决方案:ESM 入口重新导出 CJS 入口
// dist/index.mjs
import cjsModule from './index.cjs';
export const greet = cjsModule.greet;
export const version = cjsModule.version;
export default cjsModule;

示例 10:Node.js ESM 配置

// package.json
{
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

示例 11:CJS 与 ESM 互操作

// ESM 中导入 CJS(Node.js 自动桥接)
import pkg from 'cjs-package'; // 拿到 module.exports
const { method } = pkg;

// CJS 中导入 ESM(必须动态)
const esmModule = await import('esm-package');

// 注意事项:
// 1. ESM 中不能用 require()(除非动态 polyfill)
// 2. CJS 中不能用 import 语法(必须用 import())
// 3. ESM 导入 CJS 时,命名导出需要手动解构
// 4. CJS 的 module.exports 被视为 ESM 的 default 导出

常见问题与踩坑

问题原因解决方案
ESM 中 __dirname 未定义ESM 没有 CJS 的全局变量import.meta.url + fileURLToPath
CJS 包在 Vite 中报错Vite 只处理 ESM配置 optimizeDeps.include 预构建
循环依赖 CJS 返回 undefinedCJS 值拷贝,模块未执行完重构避免循环,或改用 ESM
浏览器 ESM 路径必须完整<script type="module"> 不解析省略扩展名写完整路径 ./utils.js,不能用 ./utils
ESM 顶层 await 阻塞后续模块顶层 await 让模块变异步,后续模块必须等待避免在频繁导入的模块中使用顶层 await
Dual Package 单例失效CJS 和 ESM 加载同一包产生两个实例ESM 入口 re-export CJS 入口,共享同一个实例
import 语句被提升ESM 的 import 是静态提升的,无论写在哪都在模块顶部执行理解提升机制,不要在 import 之前依赖副作用代码
CJS 的 this 指向 module.exportsCJS 模块顶层 this !== globalThisESM 中顶层 thisundefined,不要依赖顶层 this
exportsmodule.exports 混用直接赋值 module.exportsexports 引用失效只用一种方式导出,推荐 module.exports

最佳实践

  • 新项目一律用 ESM
  • npm 包同时提供 ESM 和 CJS 入口(exports 字段)
  • import() 做动态导入和代码分割
  • 浏览器中 <script type="module"> 默认 defer
  • package.json 中设置 "sideEffects": false 以启用 Tree Shaking
  • 避免使用 import * as 命名空间导入,防止 Tree Shaking 失效
  • ESM 中用 import.meta.url 替代 __dirname/__filename
  • 顶层 await 只用于必要的初始化场景(数据库连接、配置加载),避免滥用
  • Dual Package 场景下,确保 ESM 入口 re-export CJS 入口,避免单例问题

面试题

Q1: CommonJS 和 ES Module 有什么区别?

核心区别:1) CJS 是运行时加载(动态),ESM 是编译时确定依赖(静态);2) CJS 导出的是值拷贝,ESM 导出的是引用(live binding);3) ESM 支持 Tree Shaking,CJS 不支持;4) CJS 的 require 是同步的,ESM 的 import 是异步的;5) CJS 可动态条件导入,ESM 静态 import 必须在顶层(动态用 import());6) CJS 的 this 指向 module.exports,ESM 顶层 thisundefined

Q2: Tree Shaking 的原理是什么?为什么 CJS 不支持?

Tree Shaking 依赖 ES Module 的静态分析能力。打包工具在编译阶段分析 import/export,标记未使用的导出并在生成阶段移除。CJS 不支持因为其 require 是动态的(可在条件语句、循环中调用),打包工具无法在编译时确定哪些导出被使用,只能保守保留全部代码。

Q3: 模块循环依赖如何处理?CJS 和 ESM 行为有何不同?

CJS 循环依赖时,如果模块 A 还没执行完就被 B require,B 拿到的 A 的导出是已执行部分,未执行部分为 undefined(值拷贝)。ESM 循环依赖时,拿到的是引用绑定(live binding),即使模块未执行完,之后也能获取到最新值。建议:重构避免循环依赖,或提取共享逻辑到第三个模块。

Q4: UMD 是什么?解决了什么问题?

UMD(Universal Module Definition)是一种通用模块定义模式,通过运行时环境检测,让同一份代码同时兼容 CommonJS、AMD 和浏览器全局变量三种环境。它解决的是早期 JS 模块规范不统一的问题:一个库发布后需要同时支持 Node.js(CJS)、浏览器(全局变量/AMD),UMD 通过 typeof define/typeof module 检测自动选择正确的导出方式。随着 ESM 的普及,UMD 已不再推荐,新项目应使用 ESM + package.jsonexports 条件导出。

Q5: 为什么 Vite 比 Webpack 快?和 ESM 有什么关系?

Vite 在开发模式下利用浏览器原生 ESM 实现”无打包”开发服务器:1) 浏览器直接通过 <script type="module"> 加载源码,Vite 只需按需编译当前请求的模块;2) 依赖预构建用 esbuild(Go 编写,比 JS 编写的 Webpack 快 10-100 倍);3) 不像 Webpack 那样启动时就要打包所有模块。ESM 的静态分析能力让 Vite 能精确知道模块依赖关系,实现按需编译和 HMR 精确更新。Webpack 5 也支持了模块联邦和缓存优化,但架构上仍是全量打包。

Q6: import() 动态导入和 require() 有什么区别?

  1. import() 返回 Promise,是异步加载;require() 是同步加载;2) import() 可在 ESM 和 CJS 中使用,require() 只能在 CJS 中使用;3) import() 支持 Tree Shaking(配合 /* webpackChunkName */ 魔法注释),require() 不支持;4) import() 路径可以是变量,但无法静态分析;require() 路径也可以是变量;5) import() 主要用于代码分割和按需加载,require() 用于 Node.js 同步加载模块;6) import() 加载的是 ESM 模块,require() 加载的是 CJS 模块。

Q7: 什么是 “barrel file”?对 Tree Shaking 有什么影响?

Barrel file(桶文件)是一种将多个模块的导出集中重新导出的文件,通常命名为 index.js,如 export { A } from './a'; export { B } from './b';。它对 Tree Shaking 的影响:1) 某些打包器(早期 Webpack、Rollup 特定配置下)可能无法正确追踪 barrel file 的导出使用情况,导致整个文件被保留;2) 如果 barrel file 中有带副作用的模块(如 export './polyfill'),即使只用了一个导出,其他模块也不会被移除;3) 解决方案:设置 "sideEffects": false、使用深度导入路径(import { A } from './utils/a' 而非 import { A } from './utils')、避免在 barrel file 中导入有副作用的模块。

Q8: 顶层 await 有什么用?有什么限制?

顶层 await 允许在 ESM 模块顶层直接使用 await,无需包裹 async 函数。用途:1) 异步初始化(数据库连接、配置加载);2) 动态依赖解析;3) 资源获取后再导出(export const config = await loadConfig())。限制:1) 只能在 ESM 模块中使用("type": "module".mjs);2) 使用顶层 await 的模块变成异步模块,导入它的所有模块也会被隐式变为异步;3) 可能形成瀑布式加载(A 等待 → B 等待 → C),导致页面加载变慢;4) 不应在频繁被导入的基础模块中使用;5) 并行请求应使用 Promise.all 而非多个串行顶层 await。


相关链接: