← 中文资源

简体中文版本

Node.js — 循环 require 导致启动时拿到 undefined 导出

诊断 CommonJS 循环 require 引发的 TypeError cannot read properties of undefined,理解残缺的导出缓存,并用延迟 require 或拆分模块打破环。

阅读英文原文

症状

失败发生在模块求值阶段、第一个请求到来之前,报错信息在了解机制之前相当有误导性:TypeError: Cannot read properties of undefined (reading ‘ROLE_ADMIN’)。permissions.js 在顶层解构了 roles.js 的 ROLES,而 roles.js 又反过来 require 了 permissions.js。Node 对此有明确文档:模块在求值未完成时被 require,缓存中的 module.exports 还是初始的空对象,因此那一刻读到的导入是 undefined——尽管该文件最终确实正确导出了。

诊断思路

用 madge –circular 直接点名环路而不是靠猜;在失败模块内部打印 module.exports,看到 {} 或残缺子集即可证明“求值未完成”而非“没有导出”;再分别以不同模块作为入口启动,若每个入口都让不同模块报 undefined,就是典型的顺序敏感型部分求值问题。环的常见来源:顶层解构常量、父子类互相 require、barrel 文件 re-export 互相依赖的模块。

根因

CommonJS 在模块求值完成之前就把 module.exports 放进了缓存。require 环里后进入的模块拿到的是对方“求值到一半”时缓存的空导出对象,任何在加载期使用这些导出的代码都会读到 undefined。

修复与验证

三种方案按优先级排列:一是把共享部分抽成不依赖任何人的叶子模块,双方都只 require 叶子;二是把 require 从文件顶部移到使用它的函数内部(延迟 require),保证执行时两个模块都已求值完成;三是当互相引用的模块本质上是同一个概念时直接合并。避免对任何会反向依赖你的模块做顶层解构,改为函数内读取。验证时分别以服务器、任务进程、脚本等每个入口点做隔离启动,全部到达监听状态且零 undefined 错误;重跑 madge –circular 要求列表为空;并把 madge 检查加入 CI,让未来重新引入环的提交在到达生产前就失败。注意:延迟 require 会隐藏依赖图并带来每次调用的解析开销,结构性环路优先拆叶子模块。

代码示例

TypeError: Cannot read properties of undefined (reading 'ROLE_ADMIN')
    at Object.<anonymous> (src/permissions.js:9:32)
npx madge --circular src/
# 1) src/permissions.js -> src/roles.js -> src/permissions.js
src/roles-constants.js   <- leaf, required by both
src/permissions.js       <- requires the leaf only
src/roles.js             <- requires the leaf only
// in permissions.js
function assertAdmin(user) {
  const { ROLES } = require('./roles.js'); // lazy: cycle-safe
  return user.role === ROLES.ROLE_ADMIN;
}