モジュールの循環参照というNode.jsユーザの間ではよく知られている問題がある。
requireしたモジュールのメソッドやプロパティがundefinedになってしまうという問題だ。これは以下の条件で発生する
- モジュールaとモジュールbが互いにrequireしている
- 間に他のモジュールを挟んでいても発生する、a → b → c → d → e→ aのようなrequireでも発生
- module.exports を置き換えている
// a.js const b = require('./b'); module.exports = { print() { console.log('a'); }, exec() { b.print(); } }; // b.js const a = require('./a'); module.exports = { print() { console.log('b'); }, exec() { a.print(); } }; // index.js var a = require('./a'); a.exec(); var b = require('./b'); b.exec();
index.jsを実行すると・・・
$ node index.js b /Users/nullpon/b.js:10 a.print(); ^ TypeError: a.print is not a function at Object.exec (/Users/nullpon/b.js:10:11) at Object.<anonymous> (/Users/nullpon/index.js:5:3) at Module._compile (module.js:409:26) at Object.Module._extensions..js (module.js:416:10) at Module.load (module.js:343:32) at Function.Module._load (module.js:300:12) at Function.Module.runMain (module.js:441:10) at startup (node.js:139:18) at node.js:990:3
1はモジュール設計の問題。モジュールが互いに依存してしまうのは設計が間違っている証拠である。
どうしても直せない場合はsetImmediateで逃げるという技がある。イベントループの同一のフェーズで相互参照するモジュールをrequireしなければ回避できる。
// a.js const b = require('./b'); module.exports = { print() { console.log('a'); }, exec() { b.print(); } }; // b.js let b; setImmediate(() => { b = require('./b'); }); module.exports.print = function() { console.log('a'); }; module.exports.exec = function() { b.print(); }; // index.js var a = require('./a'); a.exec(); var b = require('./b'); setImmediate(() => { // 次tickに行かないとrequireが完了しない b.exec(); });
2に関してはexportsを置き換えるのではなく、exportsを拡張する形でモジュールを作成すると良い。サンプルのコードで言えば以下のようにすると良い。
// b.js const b = require('./b'); module.exports.print = function() { console.log('a'); }; module.exports.exec = function() { b.print(); };
ただしclassを作る場合は大抵exportsを置き換える事になるので、これで回避できないことも多い。基本的には相互依存を回避するように設計すべき。