一、前言:为什么会出现模块混用问题
在现代 Node.js 项目开发中,我们长期面临一个核心工程化问题:模块规范不统一。
Node.js 发展初期默认采用 CommonJS(CJS) 规范(require / module.exports);自 Node14 开始原生稳定支持 ESM 规范(import / export),Node16+、Node18+ 已全面普及 ESM,成为 Vite、TS、前端工程化的标准规范。
但目前 npm 生态处于新旧过渡阶段:
-
大量老旧第三方包仍为 CommonJS 格式;
-
新版工具库、框架依赖全部升级为纯 ESM;
-
自研项目存在老业务 CJS 代码 + 新功能 ESM 代码并存的场景。
由此产生大量诡异报错:require() of ES Module not supported、Cannot use import statement in a CommonJS module、默认导出丢失、命名导出不存在等。
本文聚焦 Node.js 双模块规范混合使用,讲解底层冲突原理、完整配置方案、兼容适配、工程落地规范,彻底解决模块混用报错问题。
二、CommonJS 与 ESM 核心差异(混用冲突根源)
2.1 基础语法差异
| 特性 | CommonJS(CJS) | ESM(ES6 模块) |
|---|---|---|
| 导入语法 | require() | import / import() |
| 导出语法 | module.exports / exports | export default / export 具名 |
| 加载时机 | 运行时动态加载 | 编译期静态解析、Tree-Shaking 支持 |
| 文件后缀 | 可省略,默认 .js | 必须补全 .js/.json 后缀 |
| 顶层变量 | 存在 module、exports、require、__dirname、__filename | 无内置变量,需手动兼容 |
2.2 核心冲突规则(Node 硬性限制)
Node.js 有两条不可绕过的模块混用规则,90% 报错均源于此:
-
CJS 模块可以引入 ESM 模块(需动态 import()),无法直接 require ESM;
-
ESM 模块可以直接引入 CJS 模块 ,但 CJS 的默认导出会被统一挂载到
module.exports; -
文件模块类型由
package.json的type字段决定。
三、type 字段:决定文件模块类型的核心配置
Node.js 通过 package.json 的 type 字段,判定当前项目下 .js 文件的模块规范,这是混合使用的核心开关。
3.1 type 两种取值规则
-
默认无 type / type: "commonjs"
-
所有 .js 文件默认按 CommonJS 解析
-
不支持顶层 import/export,写即报错
-
可以使用 require、module.exports
-
-
type: "module"
-
所有 .js 文件默认按 ESM 解析
-
强制使用 import/export,禁止直接使用 require
-
支持顶层 await、静态模块解析
-
文件导入必须补全后缀
-
四、两种混合场景完整适配方案(实战核心)
实际项目只有两种混用场景,下文提供可直接落地的配置与代码。
场景一:项目是 ESM(type:module),需要引入老旧 CJS 包
场景描述:新项目使用 ESM 规范,但是依赖大量 CommonJS 第三方包(axios、lodash 等旧版包)。
适配结论 :完全原生兼容,无需额外配置,ESM 可直接 import CJS 模块。
4.1 正确导入写法
javascript
// ESM 项目中引入 CJS 包(直接用)
import axios from 'axios';
// 具名导入 CJS 模块(兼容写法)
import * as lodash from 'lodash';
4.2 ESM 兼容 CJS 缺失变量方案
ESM 无 __dirname、__filename、require,如需使用 CJS 原生变量,手动兼容:
javascript
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { createRequire } from 'module';
// 兼容 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 兼容 require 方法(ESM 中手动启用 CJS 导入)
const require = createRequire(import.meta.url);
const oldModule = require('./commonjs-old.js');
场景二:项目是 CJS(默认规范),需要引入新版 ESM 包
场景描述 :老项目基于 CommonJS,升级部分依赖后,第三方包升级为纯 ESM,直接 require 报错:require() of ES Module not supported。
核心原因:CJS 运行时不支持加载静态 ESM 模块,Node 禁止同步 require ESM。
解决方案 :CJS 中使用 动态 import() 异步加载。
4.3 CJS 引入 ESM 标准写法
javascript
// 老 CJS 项目中,加载纯 ESM 新包
async function loadESM() {
// 异步动态导入 ESM 模块
const esmModule = await import('new-esm-package');
console.log(esmModule.default);
}
loadESM();
五、全局混合兼容配置(项目通用方案)
针对中大型项目CJS 旧业务 + ESM 新功能长期并存 的场景,提供一套零报错、可长期维护的混合配置方案。
5.1 统一 package.json 核心配置
推荐新项目、迭代中项目统一开启 type:module,通过兼容代码适配老旧 CJS 依赖,而非反向降级。
json
{
"type": "module",
"main": "./index.js",
"module": "./index.js",
"exports": {
".": "./index.js"
}
}
5.2 解决后缀名报错配置
ESM 强制要求文件后缀,不想修改源码可配置别名兼容(vite / webpack / node 均可适配),Node 原生可通过自定义 resolve 规避。
5.3 目录隔离方案(最佳工程实践)
为彻底规避混用混乱,推荐目录隔离规范:
-
/src:全部 ESM 规范,使用 import/export -
/legacy:存放老旧 CJS 代码,通过异步 import 被 src 层调用
六、TS 项目混合模块适配配置(高频场景)
TypeScript 项目是模块混用重灾区,只需修改tsconfig.json 即可完美适配双规范。
json
{
"compilerOptions": {
// 输出 ESM 规范代码
"module": "ESNext",
// 编译后语法兼容 Node18+
"target": "ES2022",
// 解析规则适配 ESM
"moduleResolution": "NodeNext",
// 允许引入 CommonJS 模块
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
}
核心作用:开启 esModuleInterop 后,TS 自动抹平 CJS 与 ESM 默认导出差异,杜绝导出 undefined 问题。
七、高频报错问题与根治方案
报错1:require() of ES Module not supported
原因:CJS 模块同步 require 纯 ESM 包
解决 :替换为 await import() 异步动态导入
报错2:Cannot use import statement in a CommonJS module
原因:文件无 type:module,被 Node 识别为 CJS,无法使用 import
解决 :根目录添加 "type":"module"
报错3:导入 CJS 模块,默认导出为 undefined
原因:CJS 模块导出挂载在 module.exports,ESM 解析规则差异
解决 :开启 esModuleInterop 或使用 import * as xxx 整体导入
报错4:相对路径导入提示模块找不到
原因:ESM 必须补全 .js/.json 后缀
解决:统一添加文件后缀,或配置构建工具自动补全
八、企业级混合模块开发最佳实践
-
新项目统一 ESM:所有 Vite/TS/Vue3/React 项目强制开启 type:module,跟随现代规范
-
老项目渐进式迁移:CJS 老项目不整体重构,新页面新逻辑全部使用 ESM,通过动态 import 互通
-
禁止混用语法:单个文件内不允许同时出现 require 和 import,语法统一
-
版本兜底:混合模块开发最低 Node16+,推荐 Node18 LTS
-
TS 必开兼容配置:esModuleInterop 开启,抹平双模块导出差异
-
目录隔离:新旧模块目录分离,降低维护成本与报错概率
九、总结
Node.js 模块混合使用的核心本质:ESM 向下兼容 CJS,CJS 无法向上同步兼容 ESM。
所有混合场景只需记住两套万能规则:
-
ESM 项目引 CJS:直接 import,手动兼容 __dirname/require 即可;
-
CJS 项目引 ESM:必须使用异步 await import(),禁止同步 require。
通过本文的 type 配置、TS 兼容方案、目录隔离规范,可以彻底解决 Node.js 双模块混用的所有报错,实现新旧项目平稳迭代、无缝兼容。