วิธีจัดการกับการพึ่งพาแบบวนซ้ำใน Node.js


162

ฉันได้ทำงานกับ nodejs เมื่อเร็ว ๆ นี้และยังคงจับกับระบบโมดูลดังนั้นขอโทษถ้านี่เป็นคำถามที่ชัดเจน ฉันต้องการรหัสคร่าวๆดังต่อไปนี้:

a.js (ไฟล์หลักทำงานกับโหนด)

var ClassB = require("./b");

var ClassA = function() {
    this.thing = new ClassB();
    this.property = 5;
}

var a = new ClassA();

module.exports = a;

b.js

var a = require("./a");

var ClassB = function() {
}

ClassB.prototype.doSomethingLater() {
    util.log(a.property);
}

module.exports = ClassB;

ปัญหาของฉันดูเหมือนว่าฉันไม่สามารถเข้าถึงอินสแตนซ์ของ ClassA จากภายในอินสแตนซ์ของ ClassB

มีวิธีที่ถูกต้อง / ดีกว่าในการจัดโครงสร้างโมดูลเพื่อให้บรรลุสิ่งที่ฉันต้องการหรือไม่? มีวิธีที่ดีกว่าในการแบ่งปันตัวแปรระหว่างโมดูลหรือไม่


ฉันขอแนะนำให้คุณดูในการแยกคำสั่งแบบสอบถามรูปแบบที่สังเกตได้แล้วสิ่งที่พวก CS เรียกผู้จัดการ - ซึ่งโดยทั่วไปแล้วเป็น wrapper สำหรับรูปแบบที่สังเกตได้
dewwwald

คำตอบ:


86

แม้ว่า node.js จะอนุญาตให้มีการrequireขึ้นต่อกันแบบวนรอบแต่คุณอาจพบว่ามันยุ่งเหยิงและคุณควรปรับโครงสร้างโค้ดของคุณให้ดีขึ้นโดยไม่ต้องการมัน อาจสร้างคลาสที่สามที่ใช้อีกสองประเภทเพื่อบรรลุสิ่งที่คุณต้องการ


6
+1 นี่คือคำตอบที่ถูกต้อง การขึ้นต่อกันแบบวงกลมเป็นรหัสกลิ่น หากใช้ A และ B ร่วมกันพวกเขาจะเป็นโมดูลเดียวได้อย่างมีประสิทธิภาพดังนั้นผสานพวกเขา หรือหาวิธีที่จะทำลายการพึ่งพา; อาจเป็นรูปแบบคอมโพสิต
James

94
ไม่เสมอ. ในโมเดลฐานข้อมูลตัวอย่างเช่นถ้าฉันมีโมเดล A และ B ใน model AI อาจต้องการอ้างอิงโมเดล B (เช่นเพื่อเข้าร่วมการดำเนินการ) และในทางกลับกัน ดังนั้นการส่งออกคุณสมบัติ A และ B หลาย ๆ อัน (อันที่ไม่ได้ขึ้นอยู่กับโมดูลอื่น) ก่อนที่จะใช้ฟังก์ชั่น "ต้องการ" อาจเป็นคำตอบที่ดีกว่า
João Bruno Abou Hatem de Liz

11
ฉันยังไม่เห็นการพึ่งพาแบบวนเป็นกลิ่นรหัส ฉันกำลังพัฒนาระบบที่มีบางกรณีที่จำเป็น ตัวอย่างเช่นการสร้างแบบจำลองทีมและผู้ใช้ที่ผู้ใช้สามารถเป็นของหลายทีม ดังนั้นไม่ใช่ว่ามีบางอย่างผิดปกติกับแบบจำลองของฉัน เห็นได้ชัดว่าฉันสามารถ refactor รหัสของฉันเพื่อหลีกเลี่ยงการพึ่งพาแบบวงกลมระหว่างสองเอนทิตี แต่นั่นไม่ใช่รูปแบบโดเมนที่บริสุทธิ์ที่สุดดังนั้นฉันจะไม่ทำเช่นนั้น
Alexandre Martini

1
ถ้าอย่างนั้นฉันควรฉีดการพึ่งพาเมื่อจำเป็นนั่นคือสิ่งที่คุณหมายถึง? ใช้หนึ่งในสามเพื่อควบคุมการโต้ตอบระหว่างสองอ้างอิงกับปัญหาวงจร?
giovannipds

2
นี้ไม่ยุ่ง .. ใครบางคนอาจต้องการเบรกไฟล์เพื่อหลีกเลี่ยงหนังสือรหัส ia ไฟล์เดียว ตามที่โหนดแนะนำคุณควรเพิ่มexports = {}ที่ด้านบนของรหัสของคุณและจากนั้นexports = yourDataในตอนท้ายของรหัสของคุณ ด้วยวิธีนี้คุณจะหลีกเลี่ยงข้อผิดพลาดเกือบทั้งหมดจากการพึ่งพาแบบวนรอบ
Prieston

178

ลองตั้งค่าคุณสมบัติบนmodule.exportsแทนการแทนที่โดยสมบูรณ์ เช่นmodule.exports.instance = new ClassA()ในa.js, ในmodule.exports.ClassB = ClassB b.jsเมื่อคุณทำการอ้างอิงโมดูลแบบวงกลมโมดูลที่ต้องการจะได้รับการอ้างอิงถึงความไม่สมบูรณ์module.exportsจากโมดูลที่ต้องการซึ่งคุณสามารถเพิ่มคุณสมบัติอื่น ๆ ได้ แต่เมื่อคุณตั้งค่าทั้งหมดmodule.exportsคุณจะสร้างวัตถุใหม่ซึ่งโมดูลที่ต้องการไม่มี วิธีการเข้าถึง


6
นี่อาจเป็นจริงทั้งหมด แต่ฉันจะบอกว่ายังคงหลีกเลี่ยงการอ้างอิงแบบวงกลม การเตรียมการพิเศษเพื่อจัดการกับโมดูลที่โหลดเสียงไม่สมบูรณ์เหมือนจะสร้างปัญหาในอนาคตที่คุณไม่ต้องการ คำตอบนี้กำหนดวิธีการจัดการกับโมดูลที่โหลดไม่สมบูรณ์ ... ฉันไม่คิดว่าเป็นความคิดที่ดี
Alexander Mills

1
คุณจะวางคอนสตรัคเตอร์ในคลาสmodule.exportsโดยไม่แทนที่มันอย่างสมบูรณ์เพื่อให้คลาสอื่น ๆ 'สร้าง' อินสแตนซ์ของคลาสได้อย่างไร
ทิมVisée

1
ฉันไม่คิดว่าคุณจะทำได้ โมดูลที่นำเข้าโมดูลของคุณแล้วจะไม่สามารถเห็นการเปลี่ยนแปลงนั้นได้
lanzz

52

[แก้ไข] ไม่ใช่ปี 2015 และห้องสมุดส่วนใหญ่ (เช่นด่วน) ได้ทำการอัปเดตด้วยรูปแบบที่ดีขึ้นดังนั้นการพึ่งพาแบบวนรอบจึงไม่จำเป็นอีกต่อไป ผมขอแนะนำให้ก็ไม่ได้ใช้พวกเขา


ฉันรู้ว่าฉันกำลังขุดคำตอบเก่า ๆ ที่นี่ ... ปัญหานี่คือ module.exports ถูกกำหนดหลังจากคุณต้องการ ClassB (ที่ลิงค์ของ JohnnyHK แสดงให้เห็น) การขึ้นต่อกันแบบวงกลมทำงานได้ดีในโหนดพวกมันเพิ่งนิยามพร้อมกัน เมื่อใช้อย่างถูกต้องพวกเขาแก้ปัญหาโหนทั่วไปจำนวนมาก (เช่นการเข้าถึง express.js appจากไฟล์อื่น ๆ )

ตรวจสอบให้แน่ใจว่ามีการกำหนดเอ็กซ์พอร์ตที่จำเป็นก่อนที่คุณต้องการไฟล์ที่มีการขึ้นต่อกันแบบวนซ้ำ

สิ่งนี้จะทำลาย:

var ClassA = function(){};
var ClassB = require('classB'); //will require ClassA, which has no exports yet

module.exports = ClassA;

สิ่งนี้จะได้ผล:

var ClassA = module.exports = function(){};
var ClassB = require('classB');

ฉันใช้รูปแบบนี้ตลอดเวลาเพื่อเข้าถึง express.js appในไฟล์อื่น ๆ :

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app

2
ขอบคุณสำหรับการแบ่งปันรูปแบบและจากนั้นแบ่งปันเพิ่มเติมว่าคุณใช้รูปแบบนี้โดยทั่วไปอย่างไรเมื่อส่งออกapp = express()
user566245

34

บางครั้งมันเป็นเรื่องเทียมที่จะแนะนำคลาสที่สาม (ตามที่ JohnnyHK แนะนำ) ดังนั้นนอกเหนือจาก Ianzz: หากคุณต้องการแทนที่ module.exports ตัวอย่างเช่นถ้าคุณกำลังสร้างคลาส (เช่นไฟล์ b.js ใน ตัวอย่างข้างต้น) สิ่งนี้เป็นไปได้เช่นกันเพียงตรวจสอบให้แน่ใจว่าในไฟล์ที่เริ่มต้นวงกลมจำเป็นต้องมีคำสั่ง 'module.exports = ... ' เกิดขึ้นก่อนที่คำสั่ง require

a.js (ไฟล์หลักทำงานกับโหนด)

var ClassB = require("./b");

var ClassA = function() {
    this.thing = new ClassB();
    this.property = 5;
}

var a = new ClassA();

module.exports = a;

b.js

var ClassB = function() {
}

ClassB.prototype.doSomethingLater() {
    util.log(a.property);
}

module.exports = ClassB;

var a = require("./a"); // <------ this is the only necessary change

ขอบคุณ coen ฉันไม่เคยรู้เลยว่า module.exports มีผลต่อการพึ่งพาแบบวงกลม
Laurent Perrin

สิ่งนี้มีประโยชน์อย่างยิ่งกับโมเดล Mongoose (MongoDB) ช่วยฉันแก้ไขปัญหาเมื่อโมเดลบล็อกโพสต์มีอาร์เรย์ที่มีการอ้างอิงถึงความคิดเห็นและแต่ละโมเดลความคิดเห็นมีการอ้างอิงถึงบล็อกโพสต์
Oleg Zarevennyi

14

การแก้ปัญหาคือ 'ส่งต่อประกาศ' วัตถุส่งออกของคุณก่อนที่จะต้องมีตัวควบคุมอื่น ๆ ดังนั้นหากคุณจัดโครงสร้างโมดูลทั้งหมดของคุณเช่นนี้และคุณจะไม่พบปัญหาใด ๆ เช่นนั้น:

// Module exports forward declaration:
module.exports = {

};

// Controllers:
var other_module = require('./other_module');

// Functions:
var foo = function () {

};

// Module exports injects:
module.exports.foo = foo;

3
จริงๆแล้วนี่ทำให้ฉันใช้exports.foo = function() {...}แทน เคล็ดลับทำอย่างแน่นอน ขอบคุณ!
zanona

ฉันไม่แน่ใจว่าคุณกำลังเสนออะไรอยู่ที่นี่ module.exportsเป็นวัตถุธรรมดาอยู่แล้วตามค่าเริ่มต้นดังนั้นบรรทัด "การประกาศไปข้างหน้า" ของคุณจึงซ้ำซ้อน
ZachB

7

ทางออกที่ต้องมีการเปลี่ยนแปลงน้อยที่สุดคือการขยายmodule.exportsแทนการเอาชนะมัน

a.js - จุดเข้าใช้งานแอพและโมดูลที่ใช้วิธีการทำจาก b.js *

_ = require('underscore'); //underscore provides extend() for shallow extend
b = require('./b'); //module `a` uses module `b`
_.extend(module.exports, {
    do: function () {
        console.log('doing a');
    }
});
b.do();//call `b.do()` which in turn will circularly call `a.do()`

b.js - โมดูลที่ใช้วิธีการทำจาก a.js

_ = require('underscore');
a = require('./a');

_.extend(module.exports, {
    do: function(){
        console.log('doing b');
        a.do();//Call `b.do()` from `a.do()` when `a` just initalized 
    }
})

มันจะทำงานและผลิต:

doing b
doing a

ในขณะที่รหัสนี้จะไม่ทำงาน:

a.js

b = require('./b');
module.exports = {
    do: function () {
        console.log('doing a');
    }
};
b.do();

b.js

a = require('./a');
module.exports = {
    do: function () {
        console.log('doing b');
    }
};
a.do();

เอาท์พุท:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function

4
หากคุณไม่มีunderscoreES6 ก็Object.assign()สามารถทำงานเดียวกันกับที่_.extend()ทำในคำตอบนี้ได้
joeytwiddle

5

ขี้เกียจต้องการอะไรเมื่อคุณต้องการ? ดังนั้น b.js ของคุณจะมีลักษณะดังนี้

var ClassB = function() {
}
ClassB.prototype.doSomethingLater() {
    var a = require("./a");    //a.js has finished by now
    util.log(a.property);
}
module.exports = ClassB;

แน่นอนว่าเป็นวิธีปฏิบัติที่ดีที่จะต้องใส่คำสั่งทั้งหมดที่ด้านบนของไฟล์ แต่มีอยู่ครั้งที่ฉันให้อภัยตัวเองสำหรับการเลือกสิ่งที่ออกจากโมดูลที่ไม่เกี่ยวข้องเป็นอย่างอื่น เรียกว่าแฮ็ค แต่บางครั้งสิ่งนี้ดีกว่าการแนะนำการพึ่งพาเพิ่มเติมหรือเพิ่มโมดูลพิเศษหรือเพิ่มโครงสร้างใหม่ (EventEmitter ฯลฯ )


และบางครั้งมันเป็นเรื่องสำคัญเมื่อต้องจัดการกับโครงสร้างข้อมูลแบบต้นไม้กับวัตถุลูกที่ยังคงอ้างอิงถึงผู้ปกครอง ขอบคุณสำหรับทิป.
Robert Oschler

5

อีกวิธีที่ฉันเห็นคนทำคือส่งออกที่บรรทัดแรกและบันทึกเป็นตัวแปรท้องถิ่นเช่นนี้

let self = module.exports = {};

const a = require('./a');

// Exporting the necessary functions
self.func = function() { ... }

ฉันมักจะใช้วิธีนี้คุณรู้เกี่ยวกับข้อเสียของมันหรือไม่?


คุณค่อนข้างสามารถทำmodule.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal

4

คุณสามารถแก้ปัญหานี้ได้อย่างง่ายดายเพียงแค่ส่งออกข้อมูลของคุณก่อนที่คุณจะต้องการสิ่งใดในโมดูลที่คุณใช้ module.exports:

classA.js

class ClassA {

    constructor(){
        ClassB.someMethod();
        ClassB.anotherMethod();
    };

    static someMethod () {
        console.log( 'Class A Doing someMethod' );
    };

    static anotherMethod () {
        console.log( 'Class A Doing anotherMethod' );
    };

};

module.exports = ClassA;
var ClassB = require( "./classB.js" );

let classX = new ClassA();

classB.js

class ClassB {

    constructor(){
        ClassA.someMethod();
        ClassA.anotherMethod();
    };

    static someMethod () {
        console.log( 'Class B Doing someMethod' );
    };

    static anotherMethod () {
        console.log( 'Class A Doing anotherMethod' );
    };

};

module.exports = ClassB;
var ClassA = require( "./classA.js" );

let classX = new ClassB();

3

คล้ายกับคำตอบของ lanzz และ setect ฉันใช้รูปแบบต่อไปนี้:

module.exports = Object.assign(module.exports, {
    firstMember: ___,
    secondMember: ___,
});

การObject.assign()คัดลอกสมาชิกไปยังexportsวัตถุที่ได้มอบให้กับโมดูลอื่นแล้ว

การ=มอบหมายนั้นซ้ำซ้อนอย่างมีเหตุผลเนื่องจากเป็นเพียงการตั้งค่าmodule.exportsให้กับตัวเอง แต่ฉันใช้มันเพราะช่วยให้ IDE ของฉัน (WebStorm) รับรู้ว่าfirstMemberเป็นคุณสมบัติของโมดูลนี้ดังนั้น "ไปที่ -> ประกาศ" (Cmd-B) และเครื่องมืออื่น ๆ จะทำงานจากไฟล์อื่น ๆ

รูปแบบนี้ไม่สวยมากดังนั้นฉันจะใช้เฉพาะเมื่อปัญหาการพึ่งพาแบบวนรอบต้องได้รับการแก้ไข


2

นี่เป็นวิธีแก้ปัญหาด่วนที่ฉันพบว่าใช้เต็ม

ในไฟล์ 'a.js'

let B;
class A{
  constructor(){
    process.nextTick(()=>{
      B = require('./b')
    })
  } 
}
module.exports = new A();

ในไฟล์ 'b.js' เขียนดังต่อไปนี้

let A;
class B{
  constructor(){
    process.nextTick(()=>{
      A = require('./a')
    })
  } 
}
module.exports = new B();

วิธีนี้ในการวนซ้ำครั้งถัดไปของคลาสลูปเหตุการณ์จะถูกกำหนดอย่างถูกต้องและคำสั่งที่ต้องการจะทำงานตามที่คาดไว้


1

จริง ๆ แล้วฉันต้องการการพึ่งพากับ

 var a = null;
 process.nextTick(()=>a=require("./a")); //Circular reference!

ไม่สวย แต่ใช้งานได้ เป็นที่เข้าใจและซื่อสัตย์มากกว่าการเปลี่ยน b.js (เช่นเพิ่มโมดูล modules.export เท่านั้น) ซึ่งเป็นอย่างอื่นที่สมบูรณ์แบบ


จากวิธีแก้ไขทั้งหมดในหน้านี้นี่เป็นทางเดียวที่แก้ไขปัญหาของฉันได้ ฉันลองแต่ละครั้ง
Joe Lapp

0

วิธีหนึ่งในการหลีกเลี่ยงคือการไม่ต้องการไฟล์หนึ่งในอีกไฟล์เพียงแค่ส่งเป็นอาร์กิวเมนต์ให้กับฟังก์ชั่นที่คุณต้องการในไฟล์อื่น ด้วยวิธีนี้การพึ่งพาแบบวนรอบจะไม่เกิดขึ้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.