Node.js循环依赖致模块导入为空、属性读取报错问题求助
问题根因
核心问题是CommonJS模块的循环依赖,同时代码里还有一处异步调用的语法bug:
- 两个文件存在顶层互相导入的关系:
challengeController.js顶层导入challengeRoutes.js,challengeRoutes.js顶层又导入challengeController.js - Node.js处理CommonJS循环引用时,不会等待被依赖模块完全执行完,只会返回该模块当前已经导出的内容。当
challengeRoutes加载challengeController时,challengeController刚执行到导入challengeRoutes的步骤,还没执行到module.exports赋值的逻辑,所以challengeRoutes里拿到的challengeController是初始空对象{},自然访问不到arrayLength方法,你看到的循环依赖警告、打印controller为空对象都是这个原因导致的。 - 额外bug:
getFullArray方法中调用challengeController.arrayLength()时没有加await,该方法是async异步函数,不加await会拿到Pending状态的Promise对象,直接访问属性也会触发类型错误。
你在app.js中导入controller能正常运行,是因为app.js没有被controller反向依赖,模块加载完成后controller的导出对象是完整的,不会触发循环依赖下的半成品导出问题。
修复方法
快速修复(适配现有代码结构)
- 打破顶层循环引用:把
challengeController.js中对challengeRoutes的导入,从文件顶层移到arrayLength方法内部。这样模块初始化阶段不会触发循环加载,只有方法实际执行时,两个模块都已经完成初始化,能拿到完整的导出对象。
修改后的challengeController.js代码:
// 移除文件顶层的 const challengeRoutes = require('./challengeRoutes'); module.exports = { arrayLength: async() => { // 方法内部按需导入路由模块,此时模块已完成初始化 const challengeRoutes = require('./challengeRoutes'); const lengths = await challengeRoutes.arrayLengths(); console.log(lengths) // 生成1到num的整数数组 let arrayLengthCounter = (num) => { let numbers = []; for(let i = 1; i <= num; i++){ numbers.push(i); } return numbers; } const charsLength = arrayLengthCounter(lengths[0]); const locationsLength = arrayLengthCounter(lengths[1]); const episodesLength = arrayLengthCounter(lengths[2]); return {charsLength, locationsLength, episodesLength}; }, }
- 补全异步调用的
await:修改challengeRoutes.js的getFullArray方法,调用arrayLength时加await,拿到实际返回值后再访问属性:
getFullArray: async() => { try{ // 补await,等待异步方法执行完成 const lengths = await challengeController.arrayLength(); const result = await Promise.all([ axios.get(url + '/character/' + lengths.charsLength).then((res) => res.data), axios.get(url + '/location/' + lengths.locationsLength).then((res) => res.data), axios.get(url + '/episode/' + lengths.episodesLength).then((res) => res.data), ]); return result; } catch(e){ console.log(e); } },
长期优化方案
快速修复只是规避了顶层循环引用,后续代码迭代还是容易再次出现同类问题。更合理的架构调整是把调用Rick and Morty API的公共逻辑抽成独立的service层文件:
- 新建
rickAndMortyService.js,存放arrayLengths这类直接请求第三方接口的通用方法 - controller和router都只依赖service层的方法,两者之间不直接互相导入,从根源上消除循环依赖的可能
内容的提问来源于stack exchange,提问作者Ignacio Garcia
相关产品推荐
相关产品推荐

