简短的回答
你可能有一个循环依赖,即你有一个文件a 那个requires 文件b,但是文件b 也有requires 文件a。结合使用module.exports = something(而不是修改现有的exports 对象),这会导致其中一个模块将获得一个从require 与另一个模块返回的空对象(其导出似乎都没有存在那里)。
快速修复最有可能将module.exports = { app, db } 替换为exports.app = app; exports.db = db 或Object.assign(exports, { app, db })。
至于为什么,以及如果这不是最佳解决方案还有哪些其他选择,请阅读下文。
长答案
听起来好像你有一个循环依赖问题。
让我解释一下,耐心等待,我们最终会解决您的问题。
请看,每个模块都以 module.exports 的空对象开头(也可作为 exports 使用)。如果您仅通过执行exports.something = something(或module.exports.something = something)修改此对象,那么即使在顶层存在循环依赖项,只要导出对象的属性在许多情况下都可以工作稍后才能访问。例如:
/* a.js */
const b = require('./b')
exports.getX = function () {
return b.getY() * 2
}
exports.getZ = function () {
return 5
}
/* b.js */
const a = require('./a')
exports.getY = function () {
return a.getZ() * 3
}
/* main.js */
const a = require('./a')
console.log(a.getX()) // Returns 30
这里我们有一个循环依赖(a 依赖于b,但b 也依赖于a)。然而,它确实有效。
上面的代码有效,因为当a.js被评估时,它的exports对象已经存在,然后当它需要b.js和b.js再次需要a.js时,即使a.js还没有即使完成了其顶级代码的运行,b.js 中的a 变量已经可以分配给a.js 的exports 对象。那时它是一个空对象,但当return a.getZ() * 3 行运行时,该对象将设置getZ 属性。
-
main 需要 a
-
a 需要 b
-
b 需要 a 并获得一个当前为空的对象
-
b 在其导出和返回上定义 getY
-
a 在其导出中定义 getX 和 getZ(这与 b 已经引用的完全相同的对象!)并返回
-
main 致电a.getX()
-
a.getX 致电b.getY
-
b.getY 现在只能访问 a 的属性 getZ,该属性现在确实存在(在步骤 5 中设置)。
请注意,如果我们写了const { getZ } = require('./a'),它就不会起作用,因为getZ 属性可能已经在第 3 步被访问了,而它还不存在。
但是,类似地,如果我们这样写a.js,它也会停止工作:
const b = require('./b')
function getX () {
return b.getY() * 2
}
function getZ () {
return 5
}
module.exports = { getX, getZ }
这里最大的不同是我们现在正在重新分配导出对象!所以,b 在第 3 步得到的对象是原始(空)对象,但我们随后将其重新分配给一个新对象(而不是修改现有对象),b 中的代码永远没有机会获取对该对象的引用!然后,您会遇到这种情况:require 给您一个空对象,尝试调用 a.getZ 将失败并返回 a.getZ is not a function(因为它是 undefined)。
(顺便说一下,module.exports = something 不与exports = something 相同,因为后者重新分配了本地exports 变量,并且不会更改其他模块将看到的从您的导出中看到的内容!)
我现在的假设是您在这里遇到了类似的问题 - 进行 DB 连接的文件需要 使用 DB 连接的文件,但反过来也是如此。
您现在有两个选择:
- 将
module.exports = { app, db } 替换为exports.app = app; exports.db = db 或Object.assign(exports, { app, db }) - 两者都会改变现有 导出对象而不是替换它。
- 稍后需要其中一个文件,方法是在另一个函数中需要它,或者通过创建一个占位符变量
let otherModule 并导出一个init 方法,该方法实际上使用otherModule = require('./otherModule') 填充它,然后调用这个init方法在require之后的第二步,从而解耦了模块代码的求值和依赖的require,打破了循环依赖。
第一个选项可能是解决问题的最简单、最直接的方法。但是,我不知道您的其余代码,因此可能存在其他障碍阻止此解决方案可行。
所以,让我详细解释第二个选项,因为它也可以经常解决问题。
第二个选项的例子:
/* a.js */
let b
function getX () {
return b.getY() * 2
}
function getZ () {
return 5
}
function init () {
b = require('./b')
}
module.exports = { init, getX, getZ }
/* b.js */
const a = require('./a')
function getY = function () {
return a.getZ() * 3
}
module.exports = { getY }
/* main.js */
const a = require('./a')
a.init()
console.log(a.getX()) // Returns 30
这段代码现在可以工作了即使它重新分配了module.exports,因为现在事情以不同的顺序发生了:
-
main 需要 a
-
a 将其导出对象替换为包含 init、getX 和 getZ 的对象并返回 [请注意,这部分代码之前运行得更晚]
-
main 致电 a.init()
-
a.init 需要 b
-
b 需要 a 并使用所有方法获取其最终导出对象 [请注意,之前它得到了一个尚未填充的对象,因为 a 还没有完全加载!]
-
b 将其导出对象替换为包含 getY 的对象并返回
-
a.init返回
-
main 致电a.getX()
- 从现在开始一切正常,因为
a 和 b 现在已经引用了彼此的完全填充的导出对象
实现“稍后要求”的第二个选项的另一种方法是require 已使用的导出函数中的另一个模块,但这可能有其缺点(例如重复代码,如果许多函数都需要它,并且执行速度稍慢,因为必须一遍又一遍地调用require)。在我们的示例中,这意味着这样做:
/* b.js */
function getY () {
const a = require('./a')
return a.getZ() * 3
}
module.exports = { getY }