Cách xử lý các phụ thuộc theo chu kỳ trong Node.js


162

Gần đây tôi đã làm việc với nodejs và vẫn nắm bắt được hệ thống mô-đun để xin lỗi nếu đây là một câu hỏi rõ ràng. Tôi muốn mã đại khái như sau:

a.js (tệp chính chạy với nút)

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;

Vấn đề của tôi dường như là tôi không thể truy cập vào thể hiện của ClassA từ bên trong một thể hiện của ClassB.

Có một cách chính xác / tốt hơn để cấu trúc các mô-đun để đạt được những gì tôi muốn? Có cách nào tốt hơn để chia sẻ các biến giữa các mô-đun không?


Tôi đề nghị bạn xem xét để tách lệnh truy vấn, mẫu có thể quan sát và sau đó là thứ mà các CS gọi là trình quản lý - về cơ bản là một trình bao bọc cho mẫu có thể quan sát được.
dewwwald

Câu trả lời:


86

Mặc dù node.js không cho phép các requirephụ thuộc vòng tròn , như bạn đã thấy nó có thể khá lộn xộn và có lẽ bạn nên cấu trúc lại mã của mình để không cần nó. Có thể tạo một lớp thứ ba sử dụng hai lớp kia để thực hiện những gì bạn cần.


6
+1 Đây là câu trả lời đúng. Phụ thuộc tròn là mùi mã. Nếu A và B luôn được sử dụng cùng nhau thì chúng thực sự là một mô-đun duy nhất, vì vậy hãy hợp nhất chúng. Hoặc tìm cách phá vỡ sự phụ thuộc; có lẽ nó là một mô hình tổng hợp.
James

94
Không phải lúc nào. trong các mô hình cơ sở dữ liệu, ví dụ, nếu tôi có mô hình A và B, trong mô hình AI có thể muốn tham chiếu mô hình B (ví dụ: để tham gia các hoạt động) và ngược lại. Do đó, xuất một số thuộc tính A và B (những thuộc tính không phụ thuộc vào các mô-đun khác) trước khi sử dụng chức năng "yêu cầu" có thể là một câu trả lời tốt hơn.
João Bruno Abou HHR de Liz

11
Tôi cũng không thấy phụ thuộc tròn như mùi mã. Tôi đang phát triển một hệ thống trong đó có một vài trường hợp cần thiết. Ví dụ: nhóm người mẫu và người dùng, nơi người dùng có thể thuộc về nhiều nhóm. Vì vậy, không có gì sai với mô hình của tôi. Rõ ràng, tôi có thể cấu trúc lại mã của mình để tránh sự phụ thuộc vòng tròn giữa hai thực thể, nhưng đó không phải là dạng thuần túy nhất của mô hình miền, vì vậy tôi sẽ không làm điều đó.
Alexandre Martini

1
Vậy thì tôi có nên tiêm phụ thuộc khi cần, đó có phải ý bạn không? Sử dụng một phần ba để kiểm soát sự tương tác giữa hai phụ thuộc với vấn đề tuần hoàn?
giovannipds

2
Điều này không lộn xộn .. ai đó có thể muốn phanh một tập tin để tránh một cuốn sách mã ia tập tin duy nhất. Như nút gợi ý, bạn nên thêm một exports = {}ở đầu mã của bạn và sau đó exports = yourDataở cuối mã của bạn. Với thực hành này, bạn sẽ tránh được hầu hết các lỗi từ các phụ thuộc tròn.
tiên tri

178

Cố gắng đặt thuộc tính trên module.exports, thay vì thay thế hoàn toàn. Ví dụ, module.exports.instance = new ClassA()trong a.js, module.exports.ClassB = ClassBtrong b.js. Khi bạn tạo phụ thuộc mô-đun tròn, mô-đun yêu cầu sẽ nhận được tham chiếu đến một phần chưa hoàn chỉnh module.exportstừ mô-đun được yêu cầu, bạn có thể thêm các thuộc tính khác sau, nhưng khi bạn đặt toàn bộ module.exports, bạn thực sự tạo một đối tượng mới mà mô-đun yêu cầu không có cách để truy cập.


6
Điều này có thể hoàn toàn đúng, nhưng tôi sẽ nói vẫn tránh phụ thuộc vòng tròn. Thực hiện các sắp xếp đặc biệt để đối phó với các mô-đun có âm thanh được tải không đầy đủ như nó sẽ tạo ra một vấn đề trong tương lai mà bạn không muốn có. Câu trả lời này quy định một giải pháp cho cách xử lý các mô-đun được tải không đầy đủ ... Tôi không nghĩ đó là một ý tưởng hay.
Alexander Mills

1
Làm thế nào bạn có thể đặt một hàm tạo của lớp module.exportsmà không thay thế hoàn toàn nó, để cho phép các lớp khác 'xây dựng' một thể hiện của lớp?
Tim Visée 7/8/2016

1
Tôi không nghĩ bạn có thể. Các mô-đun đã nhập mô-đun của bạn đã không thể thấy sự thay đổi đó
lanzz

52

[EDIT] không phải năm 2015 và hầu hết các thư viện (tức là express) đã thực hiện cập nhật với các mẫu tốt hơn để phụ thuộc vòng tròn không còn cần thiết nữa. Tôi khuyên bạn chỉ nên không sử dụng chúng .


Tôi biết tôi đang tìm ra một câu trả lời cũ ở đây ... Vấn đề ở đây là module.exports được xác định sau khi bạn yêu cầu ClassB. (mà liên kết của JohnnyHK cho thấy) Các phụ thuộc tròn hoạt động rất tốt trong Node, chúng chỉ được xác định đồng bộ. Khi được sử dụng đúng cách, chúng thực sự giải quyết được rất nhiều vấn đề về nút phổ biến (như truy cập express.js apptừ các tệp khác)

Chỉ cần đảm bảo xuất khẩu cần thiết của bạn được xác định trước khi bạn yêu cầu một tệp có phụ thuộc vòng tròn.

Điều này sẽ phá vỡ:

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

module.exports = ClassA;

Điều này sẽ làm việc:

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

Tôi sử dụng mẫu này mọi lúc để truy cập express.js apptrong các tệp khác:

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

2
cảm ơn bạn đã chia sẻ mẫu và sau đó chia sẻ thêm về cách bạn thường sử dụng mẫu này khi xuất raapp = express()
user566245

34

Đôi khi thật là giả tạo khi giới thiệu lớp thứ ba (như JohnnyHK khuyên), vì vậy, ngoài Ianzz: Nếu bạn muốn thay thế module.exports, ví dụ nếu bạn đang tạo một lớp (như tệp b.js trong ví dụ trên), điều này cũng có thể, chỉ cần đảm bảo rằng trong tệp bắt đầu thông tư yêu cầu, câu lệnh 'module.exports = ...' xảy ra trước câu lệnh yêu cầu.

a.js (tệp chính chạy với nút)

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

cảm ơn coen, tôi chưa bao giờ nhận ra rằng module.exports có ảnh hưởng đến các phụ thuộc vòng tròn.
Laurent Perrin

điều này đặc biệt hữu ích với các mô hình Mongoose (MongoDB); giúp tôi khắc phục sự cố khi mô hình BlogPost có một mảng có tham chiếu đến các nhận xét và mỗi mô hình Nhận xét có tham chiếu đến BlogPost.
Oleg Zarevennyi

14

Giải pháp là 'chuyển tiếp khai báo' đối tượng xuất khẩu của bạn trước khi yêu cầu bất kỳ bộ điều khiển nào khác. Vì vậy, nếu bạn cấu trúc tất cả các mô-đun của mình như thế này và bạn sẽ không gặp phải bất kỳ vấn đề nào như thế:

// 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
Trên thực tế, điều này dẫn tôi chỉ đơn giản là sử dụng exports.foo = function() {...}thay thế. Chắc chắn đã làm được mẹo. Cảm ơn!
zanona

Tôi không chắc chắn những gì bạn đề xuất ở đây. module.exportsđã là một đối tượng đơn giản theo mặc định, vì vậy dòng "khai báo chuyển tiếp" của bạn là dự phòng.
ZachB

7

Một giải pháp đòi hỏi thay đổi tối thiểu là mở rộng module.exportsthay vì ghi đè lên nó.

a.js - điểm nhập ứng dụng và mô-đun sử dụng phương thức làm từ 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 - mô-đun sử dụng phương thức làm từ 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 
    }
})

Nó sẽ hoạt động và sản xuất:

doing b
doing a

Trong khi mã này sẽ không hoạt động:

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

Đầu ra:

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

4
Nếu bạn không có underscore, thì ES6 Object.assign()có thể thực hiện cùng một công việc _.extend()đang làm trong câu trả lời này.
joeytwiddle

5

Những gì về lười biếng chỉ yêu cầu khi bạn cần? Vì vậy, b.js của bạn trông như sau

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

Tất nhiên, đó là cách thực hành tốt để đặt tất cả các câu lệnh yêu cầu lên trên cùng của tệp. Nhưng có những dịp, tôi tha thứ cho chính mình vì đã chọn thứ gì đó từ một mô-đun không liên quan. Gọi nó là hack, nhưng đôi khi điều này tốt hơn là giới thiệu thêm một phụ thuộc hoặc thêm một mô-đun bổ sung hoặc thêm các cấu trúc mới (EventEuctor, v.v.)


Và đôi khi nó rất quan trọng khi xử lý cấu trúc dữ liệu cây với các đối tượng con duy trì các tham chiếu đến cha mẹ. Cảm ơn vì tiền hỗ trợ.
Robert Oschler

5

Một phương pháp khác mà tôi thấy mọi người thực hiện là xuất ở dòng đầu tiên và lưu nó dưới dạng một biến cục bộ như thế này:

let self = module.exports = {};

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

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

Tôi có xu hướng sử dụng phương pháp này, bạn có biết về bất kỳ nhược điểm nào của nó không?


bạn có thể làm module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal

4

Bạn có thể giải quyết vấn đề này một cách dễ dàng: chỉ cần xuất dữ liệu của bạn trước khi bạn yêu cầu bất kỳ thứ gì khác trong các mô-đun nơi bạn sử dụng 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

Tương tự như câu trả lời của lanzz và setect, tôi đã sử dụng mẫu sau:

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

Các Object.assign()bản sao các thành viên vào exportsđối tượng đã được trao cho các mô-đun khác.

Các =nhiệm vụ là hợp lý không cần thiết, vì nó chỉ được thiết lập module.exportsvới chính nó, nhưng tôi đang sử dụng nó bởi vì nó giúp IDE của tôi (WebStorm) để nhận ra rằng firstMemberlà một tài sản của mô-đun này, vì vậy "Go To -> Tuyên bố" (Cmd-B) và các công cụ khác sẽ làm việc từ các tập tin khác.

Mẫu này không đẹp lắm, vì vậy tôi chỉ sử dụng nó khi cần giải quyết vấn đề phụ thuộc theo chu kỳ.


2

Đây là một cách giải quyết nhanh mà tôi đã thấy sử dụng đầy đủ.

Trên tệp 'a.js'

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

Trên tệp 'b.js' hãy viết như sau

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

Cách này trong lần lặp tiếp theo của các lớp vòng lặp sự kiện sẽ được xác định chính xác và những câu lệnh yêu cầu sẽ hoạt động như mong đợi.


1

Trên thực tế tôi đã kết thúc đòi hỏi sự phụ thuộc của tôi với

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

không đẹp, nhưng nó hoạt động. Điều này dễ hiểu và trung thực hơn là thay đổi b.js (ví dụ: chỉ tăng các mô-đun.export), nếu không thì hoàn hảo.


Trong tất cả các giải pháp trên trang này, đây là giải pháp duy nhất giải quyết vấn đề của tôi. Tôi lần lượt thử từng cái.
Joe Lapp

0

Một cách để tránh nó là không yêu cầu một tệp khác, chỉ chuyển nó làm đối số cho một chức năng mà bạn cần trong một tệp khác. Bằng cách này, sự phụ thuộc tròn sẽ không bao giờ phát sinh.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.