You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的导出对象是完整的,不会触发循环依赖下的半成品导出问题。

修复方法

快速修复(适配现有代码结构)

  1. 打破顶层循环引用:把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};
    },
}
  1. 补全异步调用的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 03:06:12