How to deal with cyclic dependencies in Node.js

2019-01-01 03:54发布

问题:

I\'ve been working with nodejs lately and still getting to grips with the module system so apologies if this is an obvious question. I want code roughly like the following below:

a.js (the main file run with 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;

My problem seems to be that I can\'t access the instance of ClassA from within an instance of ClassB.

Is there a correct / better way to structure modules to achieve what I want? Is there a better way to share variables between modules?

回答1:

While node.js does allow circular require dependencies, as you\'ve found it can be pretty messy and you\'re probably better off restructuring your code to not need it. Maybe create a third class that uses the other two to accomplish what you need.



回答2:

Try to set properties on module.exports, instead of replacing it completely. E.g., module.exports.instance = new ClassA() in a.js, module.exports.ClassB = ClassB in b.js. When you make circular module dependencies, the requiring module will get a reference to an incomplete module.exports from the required module, which you can add other properties latter on, but when you set the entire module.exports, you actually create a new object which the requiring module has no way to access.



回答3:

[EDIT] it\'s not 2015 and most libraries (i.e. express) have made updates with better patterns so circular dependencies are no longer necessary. I recommend simply not using them.


I know I\'m digging up an old answer here... The issue here is that module.exports is defined after you require ClassB. (which JohnnyHK\'s link shows) Circular dependencies work great in Node, they\'re just defined synchronously. When used properly, they actually solve a lot of common node issues (like accessing express.js app from other files)

Just make sure your necessary exports are defined before you require a file with a circular dependency.

This will break:

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

module.exports = ClassA;

This will work:

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

I use this pattern all the time for accessing the express.js app in other files:

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


回答4:

Sometimes it is really artificial to introduce a third class (as JohnnyHK advises), so in addition to Ianzz: If you do want to replace the module.exports, for example if you\'re creating a class (like the b.js file in the above example), this is possible as well, just make sure that in the file that is starting the circular require, the \'module.exports = ...\' statement happens before the require statement.

a.js (the main file run with 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


回答5:

The solution is to \'forward declare\' your exports object before requiring any other controller. So if you structure all your modules like this and you won\'t run into any issues like that:

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

};

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

// Functions:
var foo = function () {

};

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


回答6:

A solution which require minimal change is extending module.exports instead of overriding it.

a.js - app entry point and module which use method do from 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 - module which use method do from 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 
    }
})

It will work and produce:

doing b
doing a

While this code will not work:

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

Output:

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


回答7:

What about lazy requiring only when you need to? So your b.js looks as follows

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

Of course it is good practice to put all require statements on top of the file. But there are occasions, where I forgive myself for picking something out of an otherwise unrelated module. Call it a hack, but sometimes this is better than introducing a further dependency, or adding an extra module or adding new structures (EventEmitter, etc)



回答8:

An other method I\'ve seen people do is exporting at the first line and saving it as a local variable like this:

let self = module.exports = {};

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

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

I tend to use this method, do you know about any downsides of it?



回答9:

Actually I ended up requiring my dependency with

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

not pretty, but it works. It is more understandable and honest than changing b.js (for example only augmenting modules.export), which otherwise is perfect as is.



回答10:

Similar to lanzz and setect\'s answers, I have been using the following pattern:

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

The Object.assign() copies the members into the exports object that has already been given to other modules.

The = assignment is logically redundant, since it is just setting module.exports to itself, but I am using it because it helps my IDE (WebStorm) to recognise that firstMember is a property of this module, so \"Go To -> Declaration\" (Cmd-B) and other tooling will work from other files.

This pattern is not very pretty, so I only use it when a cyclic dependency issue needs to be resolved.



回答11:

You can solve this easily: just export your data before you require anything else in modules where you use 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();


回答12:

for your problem, you can use function declarations.

class-b.js:

var ClassA = require(\'./class-a\')

module.exports = ClassB

function ClassB() {

}

class-a.js:

var classB = require(\'./class-b\')

module.exports = ClassA

function ClassA() {

}