Node.js require nanoid 报 ERR_REQUIRE_ESM?v5 改纯 ESM 的替代方案
在 CommonJS 项目里 require('nanoid') 生成唯一 ID,进程一启动就抛 ERR_REQUIRE_ESM 直接退出。
在为客户开发 电商数据采集工具 时遇到此问题——浏览器端实时抓取商品图片、SKU、价格与评价数据,清洗后导出结构化文件,服务端需要为每条请求生成稳定的 traceId 做跨服务日志关联。
TL;DR
nanoid 从 v5 起改为纯 ESM 包,CommonJS 的 require() 无法加载它,必然抛 ERR_REQUIRE_ESM。如果你的项目还是 CJS,最省事的替代是 Node 内置的 crypto.randomUUID()——零依赖、CJS/ESM 通吃、生成的就是标准 UUID。
问题现象
CJS 项目里一行最普通的引入:
// server.js(CommonJS)
const { nanoid } = require('nanoid');
const traceId = nanoid();
启动即崩,堆栈指向 nanoid 的入口文件:
node server.js
internal/modules/cjs/loader.js:905
Error [ERR_REQUIRE_ESM]: require() of ES Module
/node_modules/nanoid/index.js from server.js not supported.
Instead change the require of index.js in server.js to a CommonJS module,
or use a dynamic import() call.
注意它不是「偶尔报错」或「某些环境下报错」,而是确定性崩溃——只要进了 v5,CJS 这条路就走不通。
根因
nanoid 在 v5 完成了 ESM-only 迁移:包的 package.json 不再带 CommonJS 入口,只导出 ESM。Node 的 CommonJS 加载器 require() 是同步的,无法同步加载一个 ESM 模块,于是直接抛 ERR_REQUIRE_ESM。
这不是 nanoid 的 bug,而是整个生态的模块格式演进:越来越多的包选择只发 ESM(got v12+、node-fetch v3、uuid v7+ 等都一样)。只要你的宿主项目是 CommonJS,遇到这类包就会撞同一堵墙。
如果你还撞过动态 import() 找不到模块,本质也是 ESM 解析规则的问题,可以看 Node.js ESM 动态 import 报模块找不到?检查文件扩展名。
解决方案
按「改造代价从低到高」给三个方案,按需选。
方案 1(推荐):用 crypto.randomUUID()
生成唯一 ID 的场景下,nanoid 的核心价值就是「短且唯一」。但只要这个 ID 不需要拼进 URL、不需要极致缩短,标准 UUID 完全够用,而且 Node 14.17+ 内置、零依赖:
// CommonJS 与 ESM 都能直接用
const { randomUUID } = require('node:crypto');
const traceId = randomUUID();
// => '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
这一步同时解决了三个问题:
- 依赖归零:不再引入第三方包,也就不再被它的模块格式绑架;
- 格式对齐:UUID 是跨语言、跨服务的通用格式,做日志关联、数据库主键都顺手;
- CJS/ESM 通吃:
node:crypto是 Node 内置模块,两种模块系统下行为一致。
唯一要权衡的是长度——UUID 36 字符,比 nanoid() 默认的 21 字符长。对 traceId、主键这类场景,长度几乎不构成成本;如果是要拼进短链,才需要继续往下看。
方案 2:锁定 nanoid v3
nanoid 的 v3.x 是最后一个兼容 CommonJS 的大版本,require 直接可用:
// package.json —— 显式钉死 v3
{
"dependencies": {
"nanoid": "^3.3.7"
}
}
const { nanoid } = require('nanoid');
const id = nanoid(); // 21 字符短 ID
适合「就是想要短 ID、又暂时无法把项目迁到 ESM」的情况。代价是停留在旧版,拿不到 v5 的后续更新。
方案 3:异步动态 import
如果你必须用 v5,只能走 ESM 的异步加载:
// CommonJS 里用动态 import() 加载 ESM 包
async function makeId() {
const { nanoid } = await import('nanoid');
return nanoid();
}
// 调用处本身得是 async
const id = await makeId();
能用,但 nanoid 是同步生成 ID 的工具,被迫包一层 async/await 会把调用链一路传染成异步,通常不值得。
注意事项
- 同一个坑不只 nanoid 一个:
uuidv7+、node-fetchv3、gotv12+ 都是 ESM-only,CJS 项目里require它们会报一模一样的ERR_REQUIRE_ESM。判断方法是看目标包的package.json有没有"type": "module"或是否只导出"import"入口。 crypto.randomUUID()需要 Node 14.17+;如果你的运行时更老,可以用crypto.randomBytes(16).toString('hex')自行拼装。- 别用
require('nanoid')的同时又在 ESM 项目里import nanoid——混用会让依赖树里同时存在新旧两份,行为更难预测。
常见问题
为什么 Node.js 中 require('nanoid') 报 ERR_REQUIRE_ESM?
因为 nanoid 从 v5 起只发布 ESM 产物,而 Node 的 CommonJS require() 是同步加载,无法加载 ESM 模块,加载到 nanoid 入口时直接抛 ERR_REQUIRE_ESM。这是 CJS/ESM 模块系统的硬性边界,不是配置问题。
nanoid v5 还能在 CommonJS 项目里使用吗?
可以,但要么用 await import('nanoid') 异步加载(注意整条调用链会变 async),要么把版本锁定在仍兼容 CJS 的 v3.x。如果只是要一个唯一 ID,直接用 Node 内置的 crypto.randomUUID() 最省事,零依赖且两种模块系统都支持。