Node.js 模块化混合开发指南:CommonJS / ESM 混用适配方案与落地配置

一、前言:为什么会出现模块混用问题

在现代 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 supportedCannot 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% 报错均源于此:

  1. CJS 模块可以引入 ESM 模块(需动态 import()),无法直接 require ESM;

  2. ESM 模块可以直接引入 CJS 模块 ,但 CJS 的默认导出会被统一挂载到 module.exports

  3. 文件模块类型由 package.jsontype 字段决定。

三、type 字段:决定文件模块类型的核心配置

Node.js 通过 package.jsontype 字段,判定当前项目下 .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 后缀

解决:统一添加文件后缀,或配置构建工具自动补全

八、企业级混合模块开发最佳实践

  1. 新项目统一 ESM:所有 Vite/TS/Vue3/React 项目强制开启 type:module,跟随现代规范

  2. 老项目渐进式迁移:CJS 老项目不整体重构,新页面新逻辑全部使用 ESM,通过动态 import 互通

  3. 禁止混用语法:单个文件内不允许同时出现 require 和 import,语法统一

  4. 版本兜底:混合模块开发最低 Node16+,推荐 Node18 LTS

  5. TS 必开兼容配置:esModuleInterop 开启,抹平双模块导出差异

  6. 目录隔离:新旧模块目录分离,降低维护成本与报错概率

九、总结

Node.js 模块混合使用的核心本质:ESM 向下兼容 CJS,CJS 无法向上同步兼容 ESM

所有混合场景只需记住两套万能规则:

  • ESM 项目引 CJS:直接 import,手动兼容 __dirname/require 即可;

  • CJS 项目引 ESM:必须使用异步 await import(),禁止同步 require。

通过本文的 type 配置、TS 兼容方案、目录隔离规范,可以彻底解决 Node.js 双模块混用的所有报错,实现新旧项目平稳迭代、无缝兼容。

相关推荐
FungLeo3 小时前
成为全栈·Node 后端篇·分类与标签:多对多关系的建模与查询
node.js·成为全栈·分类与标签·多对多关系建模·多对多关系查询
晴天163 小时前
ES 标准、V8 引擎与 Node.js 版本联动关系全解与实战踩坑
大数据·elasticsearch·node.js
晴天164 小时前
Vite vs Webpack 全方位对比
前端·webpack·node.js
且听风吟_xincell5 小时前
LibUV:Node.js 异步能力的底层支撑
node.js
掰头战士20 小时前
从LLM到Agent、Agent的6大核心。这些基础知识你还记得吗
node.js·llm·agent
FungLeo1 天前
成为全栈·Node 后端篇·注册登录全流程实现
node.js·登录流程·成为全栈·注册流程
AI大模型-小华1 天前
Codex CLI第一次怎么用?从安装到读取本地项目完整教程
git·node.js·ai编程·开发工具·代码分析·codex·codex cli
晴天161 天前
打造自己的 npm 包实战指南
前端·npm·node.js
西西小飞龙1 天前
npm vs pnpm
前端·npm·node.js