ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Node.js 模块系统:CJS 与 ESM 详解

2026/8/8 8:18:24 拓冰建站 浏览量
Node.js 模块系统:CJS 与 ESM 详解

文章目录

  • 前言
  • 一、CommonJS (CJS)
  • 二、ESM(ES Module)
  • 三、跨模块互相导入(重要坑点)
  • 四、有关路径的要求
  • 总结

前言

Node.js 两套模块系统:CommonJS(CJS)ESM,由package.json和文件后缀共同决定。JavaScript 最初没有模块系统,Node.js 诞生后自行实现 CommonJS(require/module.exports),用于服务端,并非语言官方标准。随后,ES6 推出官方标准 ESM(import/export),支持浏览器与 Node,支持静态分析,但 Node 为兼容海量旧生态没有直接替换 CommonJS。
于是 Node 两套模块共存,依靠package.json type和文件后缀.mjs/.cjs区分,由此产生各类导入语法差异。


一、CommonJS (CJS)

要求如下:

  • package.json: “type”: “commonjs” 默认值,不写type也是CJS
  • 文件后缀:.js -> CJS; .cjs -> 强制 CJS,无视package.json
  • 语法:require() / module.exports,不能用import/export

导出(helpers.js):

// 方式1:整体导出对象(最常用)module.exports={func1,func2};// 方式2:逐个挂载exports.func1=func1;exports.func2=func2;

导入(main.js):

// ✅ 本地文件,必须 ./ 开头;后缀 .js 可以省略,建议写上const{func1,func2}=require("./helpers.js");// ❌ 错误:不带 ./,node会去node_modules查找npm包const{func1}=require("helpers");
  • require("./helpers")省略后缀也可以,Node 自动补.js/.json
  • 只能用.js后缀,不能命名为.mjs
  • 运行命令:node main.js

二、ESM(ES Module)

  • package.json"type":"module"
    • 文件后缀:.mjs→ 强制 ESM,完全忽略 package.json 的 type 配置
    • 语法:import / export,原生不能直接用require()

重点:.mjs永远 ESM;.cjs永远 CJS;普通.jspackage.jsontype字段。

导出(helpers.js):

// 方式1:声明时直接导出exportasyncfunctionloadPackage(){}// 方式2:末尾集中导出export{loadPackage};// 默认导出exportdefault{loadPackage};

导入(main.mjs):

// ✅本地文件:必须 ./ 开头,**后缀 .js 不能省略!ESM不会自动补后缀**import{loadPackage}from"./helpers.js";// 如果导入的是CJS模块(helpers.js是module.exports),也可以default导入importhelpersfrom"./helpers.js";const{loadPackage}=helpers;// ❌错误1:无 ./,当作npm包import{loadPackage}from"helpers";// ❌错误2:省略后缀,ESM直接报模块找不到import{loadPackage}from"./helpers";

当文件是.mjs,就算 package.json 是commonjs,依然执行 ESM 语法。
运行:node generateTestcase.mjs

三、跨模块互相导入(重要坑点)

  • ESM (.mjs) 导入 CJS (.js):
    允许;CJS 的module.exports对象被 ESM 识别,支持解构导入。
import{loadPackage}from"./helpers.js";// 结构导入// 或者使用CJS的原生require导入import{createRequire}from"module";constrequire=createRequire(import.meta.url);// Enable require in ESMconstpkg=require(pkgPath);
  • CJS 导入 ESM
    CJS 的require()不能直接 require ESM 文件,会报错;只能用动态await import()
// CJS里面加载ESM模块只能动态importconstesmModule=awaitimport("./some-esm.js");

四、有关路径的要求

  • ESM 动态导入,await import其路径必须是file://开头的 URL 字符串指向本地磁盘文件。不接受windows系统的反斜杠。
// ✅ 本地磁盘文件,必须转成file:// URLconsturl=pathToFileURL(absDiskPath).href;constmod=awaitimport(url);constabsFsPath="C:\\Users\\LIly\\file.js";// path.resolve得到,带反斜杠 const url = pathToFileURL(absFsPath).href; // ✅API内部自动处理反斜杠 → file:///C:/... const mod = await import(url);awaitimport("./dir/test.js");// ✅ 只能正斜杠 await import("./dir\\test.js");// ❌ 反斜杠不行,同静态import
// Windows反斜杠转换functionnormalizeSlash(p){if(isWindows()){returnp.replace(/\\/g,'/');}returnp;}
  • ESM 静态导入 import xxx from “xxx”
    静态导入不接受 file:// URL,也不接受操作系统磁盘绝对路径(C:\xxx /home/xxx)。不接受Windows的反斜杠。
    静态导入只有两类合法输入:

裸模块名(npm 包、node 内置模块):lodash、fs/promises

importxfrom"lodash"

相对说明符:./xxx.js、…/xxx.js

importxfrom"./dir\\test.js";// ❌错误!\是字符串转义符号,路径直接错乱importxfrom"./dir/test.js";// ✅只能正斜杠 /
  • require的动态导入,操作系统原生磁盘路径,完全接纳 Windows 反斜杠\,唯一坑:给 require 的相对本地文件路径,必须带上./或者../,否则会被识别成npm包。
letpath="./helper.js";constm=require(path);// ✅完全合法if(flag){require("./other.js")}

总结

Node.js 中 CJS 与 ESM 双模块系统共存,核心区分逻辑是package.jsontype字段 + 文件后缀(.mjs/.cjs

  • CJSrequire/module.exportstype: commonjs(或默认),require对本地文件路径带./,后缀.js可省略。
  • ESMimport/exporttype: module(或.mjs后缀强制),import必须写./后缀不可省略
  • 跨模块导入:ESM 导入 CJS 一般没问题,支持解构导入;反过来 CJS 用require加载 ESM 会直接报错,必须用动态await import()

日常开发先确认项目根目录package.jsontype,再决定用哪个文件后缀和导入语法,可以避免绝大多数模块解析错误。

导入方式类型执行时机静态 / 动态路径完整要求Windows 反斜杠支持相对路径要求
CommonJSrequire()运行时函数调用执行到该行才加载✅动态导入1. npm 包:直接写包名

2. 本地文件:操作系统原生磁盘路径;不需要 file:// 协议
✅兼容\/本地相对文件必须带.//../;不带则识别为 npm 包;后缀可省略
ESM 静态导入import xxx from "xxx"JS 语法解析阶段(代码运行前)✅静态导入1. npm 包:直接写包名

2. 本地文件:仅允许.//../相对说明符;禁止 file://、禁止磁盘绝对路径;只能字符串字面量,不能变量
❌禁止\,只能正斜杠/必须.//../前缀;必须写完整.js后缀,不可省略
ESM 动态导入await import(xxx)Promise 函数调用执行到该行才加载✅动态导入1. npm 包:直接写包名

2. 本地磁盘文件:必须传入file://URL;不能直接传操作系统磁盘路径;支持变量传参
❌不要手动处理\;原始磁盘路径交给pathToFileURL()自动转 URL字面量写./xxx.js规则同静态导入;变量加载本地文件必须转为file://