Webpack 5 将 ES Modules 的内部处理机制称为 Harmony,这个名字并不是简单的代号,而是代表了模块系统之间的一种“和谐”状态。在过去,Webpack 需要同时处理 CommonJS、AMD、UMD 和 ESM 等多种模块格式,不同格式之间的互操作经常出现隐式转换、重复包装等问题。Webpack 5 对 Harmony 模块的支持更加彻底,不仅优化了 ESM 的编译输出,还改进了 ESM 与 CommonJS 之间的互操作规则,让开发者在混合使用模块语法时不再需要关心底层差异。下面从几个核心角度深入分析 Harmony 在 Webpack 5 中的具体表现。

模块系统的和谐统一:ESM 与 CommonJS 的互操作
Webpack 5 在处理 import 和 require 混用场景时,采用了更接近原生 ECMAScript 规范的行为。在 Webpack 4 中,如果从一个 CommonJS 模块中 import 一个具名导出,例如 import { foo } from './cjs-module',Webpack 4 会尝试通过静态分析将 module.exports 的属性映射到 foo,但这种分析有时会失败或产生歧义。Webpack 5 则统一使用 __webpack_require__ 的包装层,并把 CommonJS 模块包装成类似 ESM 的命名空间对象,使得 import 语法可以稳定地从 CommonJS 模块中获取默认导出或具名导出。反过来,当一个 ESM 模块被 require 引用时,Webpack 5 会将 ESM 的导出自动包装成 module.exports 对象,并标记为不可枚举但可访问的属性,从而兼容 CommonJS 的读取方式。
这种互操作改进带来的直接好处是:开发者可以逐步将项目中的 CommonJS 代码迁移到 ESM,而不必一次性改写所有引用。例如下面这段代码在 Webpack 5 中可以正确运行,而 Webpack 4 可能需要额外配置才能避免警告:
// cjs-module.js
exports.name = 'Webpack';
exports.version = 5;
// esm-consumer.js
import { name, version } from './cjs-module';
console.log(name, version);
// 反向:在 CommonJS 中 require ESM
// esm-module.js
export const framework = 'Webpack';
export default function sayHello() {
return 'Hello Harmony';
}
// cjs-consumer.js
const { framework, default: sayHello } = require('./esm-module');
console.log(framework, sayHello());
不过需要注意,虽然 Webpack 5 的互操作很智能,但并非所有动态情况都能完美处理。如果 CommonJS 模块在运行时才修改 module.exports 的结构,例如先导出空对象再在条件分支中赋值属性,Webpack 的静态分析仍然可能漏掉这些属性,导致具名导入结果为 undefined。因此建议在混合模块类型时,优先使用默认导入导出或命名空间导入,以避免歧义。
更强的 Tree Shaking 与副作用分析
Harmony 模块系统能够高效进行 tree shaking 的前提是模块具有静态结构,即导入和导出在编译期就可确定。Webpack 5 在 Harmony 模式下对副作用(side effects)的判断比 Webpack 4 更加精准。首先,sideEffects 字段在 package.json 中的支持更加完善,开发者可以通过数组精确列出哪些文件具有副作用,Webpack 5 会严格按照这个列表决定是否删除未被引用的导出。其次,Webpack 5 改进了对函数调用副作用的分析,例如在如下代码中:
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// index.js
import { add } from './math';
console.log(add(1, 2));
Webpack 5 会在生产构建中自动删除 subtract 函数,因为它没有被引用,而且 math.js 整个模块被标记为无副作用(通过 "sideEffects": false 或 Webpack 的 optimization.sideEffects 配置)。这种删除不仅是函数级别的,还包括导出语句和对应的依赖引用。相比之下,Webpack 4 在某些嵌套导出或重导出场景下可能无法完全消除死代码。
另一个显著改进是 import.meta 的支持。Harmony 模块系统完整支持 import.meta.url 和 import.meta.webpack 等属性,这为动态加载资源、计算模块路径提供了便利。例如,一个组件希望加载同目录下的图片资源,可以这样写:
const imageUrl = new URL('./logo.png', import.meta.url);
const img = document.createElement('img');
img.src = imageUrl;
document.body.appendChild(img);
Webpack 5 会识别 new URL(..., import.meta.url) 这种模式,并在构建时生成正确的资源 URL,同时保留对模块路径的静态分析能力。如果 sideEffects 配置得当,没有被访问的资源也不会被打包。
模块联邦:跨应用共享模块的新范式
Webpack 5 引入的 Module Federation(模块联邦)是 Harmony 模块系统最引人注目的扩展。它允许一个应用在运行时从另一个独立部署的应用中动态加载模块,就像加载本地模块一样。这种能力背后的核心是 Harmony 的异步加载和依赖共享机制。通过 ModuleFederationPlugin,你可以将某个模块(例如 React 组件、工具函数)暴露给其他应用,同时声明自己需要从其他应用获取的依赖。
一个典型的模块联邦配置如下:
// webpack.config.js (host app)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remoteApp: 'remote@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true },
},
}),
],
};
在上面的配置中,host 应用声明了一个远程模块 remoteApp,它将在运行时从 http://localhost:3001/remoteEntry.js 加载。同时 shared 配置确保 React 和 ReactDOM 不会被重复加载,而是通过共享作用域在 host 和 remote 之间复用同一个实例。这种共享机制正是 Harmony 模块系统对动态依赖解析能力的体现,它避免了微前端场景下常见的重复依赖和实例不一致问题。
模块联邦的使用方式非常接近原生 ESM 的动态导入:
// 在 host 应用代码中
import('remoteApp/Button').then(({ default: Button }) => {
const root = document.getElementById('root');
root.innerHTML = Button({ text: 'Click me' });
});
Webpack 5 会将 import('remoteApp/Button') 转换为对远程入口的加载和模块获取请求,远程应用会返回对应模块的工厂函数。整个过程基于 Promise 实现,完全符合 Harmony 模块的异步语义。模块联邦还支持嵌套联邦、版本协商、单向或双向共享等高级特性,极大地提高了大型前端系统的模块复用能力。
实用配置与注意事项
虽然 Webpack 5 的 Harmony 模块系统大幅提升了开发体验,但在实际升级项目中仍然有一些配置细节需要留意。首先是 optimization.usedExports 和 optimization.sideEffects 的默认开启情况。Webpack 5 在生产模式下默认开启这两个选项,但在开发模式下默认关闭,这可能导致开发构建与生产构建在模块导出行为上出现差异。如果你在开发环境中使用 import { foo } from './module' 但 foo 实际未被导出,开发模式可能不会报错(因为未开启严格导出检查),而生产构建可能会在 tree shaking 阶段抛出错误或静默处理为 undefined。建议在开发模式也开启 optimization.usedExports: true 以尽早发现问题。
其次是 module.rules 中的 type 配置。Webpack 5 用 type: 'javascript/auto'、type: 'javascript/esm' 等区分模块类型。默认的 javascript/auto 会尝试自动检测模块是 ESM 还是 CommonJS,但某些边界情况(例如使用 eval 动态生成 import)可能判断错误。对于明确使用 ESM 编写的库,建议将其规则设置为 type: 'javascript/esm',这样 Webpack 会强制按照 Harmony 模块处理,避免意外降级为 CommonJS 解析。
还有一种常见的升级陷阱是 __webpack_require__.r 相关的运行时错误。当你在多个 entry 或动态加载的 chunk 之间共享同一个模块实例时,如果忽略了对 output.library.type 的设置,可能会得到重复的模块实例,进而导致 React 这类依赖单例的库出现“Invalid hook call”等错误。正确做法是在 output 中明确 library: { type: 'module' } 或使用 shared 配置的 singleton: true。总之,Harmony 模块系统的“和谐”并不是自动达成的,它需要开发者根据项目实际情况调整配置,才能真正享受到 Webpack 5 带来的模块处理优势。