Cara menangani dependensi siklik di Node.js

162

Saya telah bekerja dengan nodejs akhir-akhir ini dan masih memahami sistem modul jadi minta maaf jika ini adalah pertanyaan yang jelas. Saya ingin kode kira-kira seperti berikut ini:

a.js (file utama dijalankan dengan node)

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;

Masalah saya sepertinya saya tidak bisa mengakses instance dari ClassA dari instance ClassB.

Apakah ada cara yang benar / lebih baik untuk menyusun modul untuk mencapai apa yang saya inginkan? Apakah ada cara yang lebih baik untuk berbagi variabel antar modul?

Dapat diakses
sumber
Saya sarankan Anda melihat ke perintah pemisahan permintaan, pola yang dapat diamati dan kemudian apa yang orang-orang CS sebut manajer - yang pada dasarnya adalah pembungkus untuk pola yang dapat diamati.
dewwwald

Jawaban:

86

Meskipun node.js memungkinkan requiredependensi melingkar , seperti yang Anda temukan itu bisa sangat berantakan dan Anda mungkin lebih baik merestrukturisasi kode Anda untuk tidak membutuhkannya. Mungkin membuat kelas ketiga yang menggunakan dua lainnya untuk mencapai apa yang Anda butuhkan.

JohnnyHK
sumber
6
+1 Ini adalah jawaban yang tepat. Ketergantungan melingkar adalah bau kode. Jika A dan B selalu digunakan bersama-sama mereka secara efektif adalah modul tunggal, jadi gabungkan mereka. Atau temukan cara untuk memutuskan ketergantungan; mungkin itu pola gabungan.
James
94
Tidak selalu. dalam model basis data, misalnya, jika saya memiliki model A dan B, dalam model AI mungkin ingin merujuk model B (misalnya untuk bergabung dengan operasi), dan sebaliknya. Oleh karena itu, ekspor beberapa properti A dan B (yang tidak bergantung pada modul lain) sebelum menggunakan fungsi "wajib" mungkin merupakan jawaban yang lebih baik.
João Bruno Abou Hatem de Liz
11
Saya juga tidak melihat dependensi melingkar sebagai kode bau. Saya sedang mengembangkan suatu sistem di mana ada beberapa kasus di mana dibutuhkan. Misalnya, pemodelan tim dan pengguna, tempat pengguna dapat menjadi bagian dari banyak tim. Jadi, bukan berarti ada yang salah dengan pemodelan saya. Jelas, saya bisa memperbaiki kode saya untuk menghindari ketergantungan melingkar antara dua entitas, tapi itu bukan bentuk paling murni dari model domain, jadi saya tidak akan melakukan itu.
Alexandre Martini
1
Maka haruskah saya menyuntikkan ketergantungan ketika dibutuhkan, apakah itu yang Anda maksud? Menggunakan yang ketiga untuk mengontrol interaksi antara dua dependensi dengan masalah siklik?
giovannipds
2
Ini tidak berantakan .. seseorang mungkin ingin rem file untuk menghindari buku kode besarbesaran file. Sebagai node menyarankan Anda harus menambahkan exports = {}di bagian atas kode Anda dan kemudian exports = yourDatadi akhir kode Anda. Dengan latihan ini Anda akan menghindari hampir semua kesalahan dari dependensi melingkar.
prieston
178

Cobalah untuk mengaktifkan properti module.exports, alih-alih menggantinya sepenuhnya. Misalnya, module.exports.instance = new ClassA()dalam a.js, module.exports.ClassB = ClassBdalam b.js. Ketika Anda membuat dependensi modul melingkar, modul yang membutuhkan akan mendapatkan referensi ke yang tidak lengkap module.exportsdari modul yang diperlukan, yang Anda dapat menambahkan properti lain yang terakhir, tetapi ketika Anda mengatur keseluruhan module.exports, Anda benar-benar membuat objek baru yang modul yang membutuhkan tidak memiliki cara mengakses.

lanzz
sumber
6
Ini mungkin benar, tetapi saya akan mengatakan masih menghindari ketergantungan melingkar. Membuat pengaturan khusus untuk menangani modul yang memuat suara tidak lengkap seperti itu akan menciptakan masalah di masa depan yang tidak Anda inginkan. Jawaban ini menentukan solusi untuk bagaimana menangani modul yang dimuat tidak lengkap ... Saya tidak berpikir itu ide yang bagus.
Alexander Mills
1
Bagaimana Anda memasukkan konstruktor kelas module.exportstanpa sepenuhnya menggantinya, untuk memungkinkan kelas lain 'membangun' contoh kelas?
Tim Visée
1
Saya pikir kamu tidak bisa. Modul yang telah mengimpor modul Anda sudah tidak akan dapat melihat perubahan itu
lanzz
52

[EDIT] ini bukan tahun 2015 dan sebagian besar perpustakaan (yaitu express) telah membuat pembaruan dengan pola yang lebih baik sehingga dependensi melingkar tidak lagi diperlukan. Saya sarankan tidak menggunakannya .


Saya tahu saya sedang menggali jawaban lama di sini ... Masalahnya di sini adalah module.exports didefinisikan setelah Anda memerlukan ClassB. (yang ditunjukkan oleh tautan JohnnyHK) Ketergantungan melingkar bekerja dengan baik di Node, mereka hanya didefinisikan secara sinkron. Ketika digunakan dengan benar, mereka sebenarnya memecahkan banyak masalah simpul umum (seperti mengakses express.jsapp dari file lain)

Pastikan ekspor yang diperlukan sudah ditentukan sebelumnya Anda memerlukan file dengan ketergantungan melingkar.

Ini akan merusak:

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

module.exports = ClassA;

Ini akan berhasil:

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

Saya menggunakan pola ini setiap saat untuk mengakses express.js appdi file lain:

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app
Will Stern
sumber
2
terima kasih telah membagikan pola dan kemudian membagikan lebih lanjut bagaimana Anda biasanya menggunakan pola ini saat mengeksporapp = express()
user566245
34

Kadang-kadang sangat buatan untuk memperkenalkan kelas ketiga (seperti yang disarankan JohnnyHK), jadi selain Ianzz: Jika Anda ingin mengganti module.exports, misalnya jika Anda membuat kelas (seperti file b.js di contoh di atas), ini dimungkinkan juga, hanya pastikan bahwa dalam file yang memulai lingkaran memerlukan, pernyataan 'module.exports = ...' terjadi sebelum pernyataan memerlukan.

a.js (file utama dijalankan dengan node)

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
sumber
terima kasih coen, saya tidak pernah menyadari bahwa module.exports memiliki efek pada dependensi melingkar.
Laurent Perrin
ini sangat berguna dengan model Mongoose (MongoDB); membantu saya untuk memperbaiki masalah ketika model BlogPost memiliki larik dengan referensi ke komentar, dan setiap model Komentar memiliki referensi ke BlogPost.
Oleg Zarevennyi
14

Solusinya adalah 'meneruskan menyatakan' objek ekspor Anda sebelum memerlukan pengontrol lain. Jadi jika Anda menyusun semua modul Anda seperti ini dan Anda tidak akan mengalami masalah seperti itu:

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

};

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

// Functions:
var foo = function () {

};

// Module exports injects:
module.exports.foo = foo;
Nicolas Gramlich
sumber
3
Sebenarnya, ini membuat saya hanya menggunakan exports.foo = function() {...}saja. Pasti berhasil. Terima kasih!
zanona
Saya tidak yakin apa yang Anda usulkan di sini. module.exportssudah menjadi Obyek biasa secara default, jadi baris "deklarasi maju" Anda berlebihan.
ZachB
7

Solusi yang membutuhkan perubahan minimal adalah memperluas module.exportsalih-alih menimpanya.

a.js - titik masuk aplikasi dan modul yang menggunakan metode lakukan dari 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 - modul yang menggunakan metode do dari 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 
    }
})

Ini akan bekerja dan menghasilkan:

doing b
doing a

Sementara kode ini tidak akan berfungsi:

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();

Keluaran:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function
setec
sumber
4
Jika Anda tidak memilikinya underscore, maka ES6 Object.assign()dapat melakukan pekerjaan yang sama dengan yang _.extend()dilakukan dalam jawaban ini.
joeytwiddle
5

Bagaimana dengan malas yang hanya membutuhkan saat Anda perlu? Jadi b.js Anda terlihat sebagai berikut

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

Tentu saja merupakan praktik yang baik untuk meletakkan semua pernyataan yang diperlukan di atas file. Tetapi ada beberapa kesempatan, di mana saya memaafkan diri sendiri karena memilih sesuatu dari modul yang tidak berhubungan. Sebut saja hack, tetapi kadang-kadang ini lebih baik daripada memperkenalkan dependensi lebih lanjut, atau menambahkan modul tambahan atau menambahkan struktur baru (EventEmitter, dll)

zevero
sumber
Dan kadang-kadang sangat penting ketika berhadapan dengan struktur data pohon dengan objek anak yang mempertahankan referensi ke orangtua. Terima kasih atas tipnya.
Robert Oschler
5

Metode lain yang saya lihat orang lakukan adalah mengekspor di baris pertama dan menyimpannya sebagai variabel lokal seperti ini:

let self = module.exports = {};

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

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

Saya cenderung menggunakan metode ini, apakah Anda tahu tentang kelemahannya?

Bence Gedai
sumber
Anda bisa melakukannya module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal
4

Anda dapat menyelesaikan ini dengan mudah: cukup ekspor data Anda sebelum Anda membutuhkan hal lain dalam modul tempat Anda menggunakan 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();
Giuseppe Canale
sumber
3

Mirip dengan jawaban lanzz dan setect, saya telah menggunakan pola berikut:

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

Para Object.assign()salinan anggota keexports objek yang telah diberikan kepada modul lain.

The =tugas secara logis berlebihan, karena itu hanya menetapkan module.exportsuntuk dirinya sendiri, tapi saya menggunakannya karena membantu IDE saya (WebStorm) untuk mengakui bahwafirstMember adalah properti dari modul ini, sehingga "Go To -> Deklarasi" (Cmd-B) dan perkakas lain akan bekerja dari file lain.

Pola ini tidak terlalu cantik, jadi saya hanya menggunakannya ketika masalah ketergantungan siklik perlu diselesaikan.

joeytwiddle
sumber
2

Ini adalah solusi cepat yang menurut saya cukup bermanfaat.

Di file 'a.js'

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

Pada file 'b.js' tulis yang berikut ini

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

Dengan cara ini pada iterasi berikutnya dari kelas-kelas loop peristiwa akan didefinisikan dengan benar dan pernyataan-pernyataan yang membutuhkan akan bekerja seperti yang diharapkan.

Melik Karapetyan
sumber
1

Sebenarnya saya akhirnya membutuhkan ketergantungan dengan

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

tidak cantik, tapi berhasil. Ini lebih dimengerti dan jujur ​​daripada mengubah b.js (misalnya hanya menambah modules.export), yang sebaliknya sempurna seperti apa adanya.

zevero
sumber
Dari semua solusi di halaman ini, ini adalah satu-satunya yang menyelesaikan masalah saya. Saya mencoba masing-masing secara bergantian.
Joe Lapp
0

Salah satu cara untuk menghindarinya adalah dengan tidak memerlukan satu file di lain hanya meneruskannya sebagai argumen ke fungsi apa pun yang Anda butuhkan di file lain. Dengan cara ini ketergantungan sirkular tidak akan pernah muncul.

sagar saini
sumber