node.js 14+ 启用原生esm需满足:package.json设"type":"module"或使用.mjs后缀;esm中导入cjs模块仅支持default,解构需createrequire;读json用import(..., {assert: {type:'json'}})或fs.readfile;__dirname需import.meta.url+fileurltopath;顶层await可用于初始化。

Node.js 14+ 中启用 ESM 的必要配置
Node.js 原生 ESM 不是开箱即用的——即使你写了 import,不加配置也会报 ERR_REQUIRE_ESM。关键在两点:文件后缀和 package.json 字段。
必须满足以下任一条件,Node 才会以 ESM 模式加载模块:
-
"type": "module"写在项目根目录package.json中(推荐,全局生效) - 或使用
.mjs后缀(如server.mjs),无视type字段 -
.cjs强制 CJS,.mjs强制 ESM,.js则由type决定
注意:type: module 下,require() 会直接报错;反之,CJS 里不能用 import。混用需靠动态 import() 或 createRequire。
ESM 下如何正确导入 CommonJS 模块
很多生态库(如 express、pg)仍是 CJS 输出,ESM 项目里直接 import express from 'express' 通常能工作,但有隐性限制:
- Node 会自动将 CJS 包封装为默认导出(
default),所以import express from 'express'可行,但import { Router } from 'express'会失败(CJS 没命名导出) - 若 CJS 模块导出的是函数(如
module.exports = function(){}),ESM 中只能通过import xxx from 'xxx'获取,不能解构 - 想安全访问 CJS 的具名属性?用动态
import()+default解构,或改用createRequire(需先import { createRequire } from 'module')
示例:
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { Pool } = require('pg'); // 这样才能解构 pg 的命名导出
ESM 中读取 JSON 和 __dirname/__filename 的替代方案
ESM 不支持 require('./config.json'),也不提供 __dirname 和 __filename —— 这是常见卡点。
- 读 JSON:用
await import('./config.json', { assert: { type: 'json' } })(Node ≥17.1),或更兼容的fs/promises.readFile('./config.json', 'utf8').then(JSON.parse) - 获取当前模块路径:用
import.meta.url,配合file://协议解析:import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); - 注意:
import.meta.url是只读字符串,不能直接当路径用,必须经fileURLToPath转换
别漏掉 import { dirname } from 'path',否则 dirname() 会 undefined。
顶层 await 和动态 import 在后端启动流程中的实用场景
ESM 允许模块顶层 await,这对后端很实用:数据库连接、配置加载、服务注册等初始化逻辑可自然串行,不用包进 async function main()。
- 例如启动 Express 服务前等待 DB 连接就绪:
const db = await connectDB(); const app = express(); - 按需加载插件或中间件:用
const authMiddleware = await import('./middleware/auth.js');,避免冷启动时全量加载 - 动态
import()返回 Promise,可用于环境分支加载:if (process.env.NODE_ENV === 'dev') await import('./dev-tools.js');
注意:顶层 await 会阻塞整个模块树的执行,慎用于耗时过长或可能失败的操作;错误会直接导致模块加载失败,需包裹 try/catch 或用 .catch() 处理。
真正麻烦的不是语法,而是混合生态下的路径解析、JSON 加载、CJS 互操作——这些地方出错时错误信息往往模糊,得盯住 import.meta.url 和 package.json#type 两个锚点反复核对。











