require() によって Node.js がどのようにロードするモジュールを検索するかは、Node.js の Modules のドキュメント に詳しく説明されていますが、若干複雑なのでここでまとめておきます。
require でモジュールをロードするとき、多くは下記の 3 パターンのロード方法に分類できます。
// コアモジュール、あるいは node_modules にインストールしたパッケージのロード
const crypto = require('crypto');
// ローカルモジュールのロード
const myLocalModule = require('./path/to/myLocalModule');
// JSON ファイルのロード
const jsonData = require('./path/to/data.json');
簡単にまとめると、
./ で始まるパスで指定する。と理解しておけばよいでしょう。
require によってどのパスに置かれたファイルがロードされるかは、下記のようなアルゴリズムで決められます。
require のパラメータでモジュール名そのもの(express など)を指定した場合は、下記のようなアルゴリズムでロードするモジュールが決められます。
name というコアモジュールを探す。node_modules/name というパッケージを探す。node_modules/name というパッケージを探していく。例えば、/aaa/bbb/main.js 内で require('express') とすると、下記のように検索されていきます。
express/aaa/bbb/node_modules/express/aaa/node_modules/express/node_modules/expressちなみに、name というモジュール名の部分に、path/to/name のようにディレクトリパスが含まれていれば、そのような階層でインストールされた name モジュールがロードされます(node_modules/path/to/name などが検索される)。
require を呼び出した場合は、何よりも先にコアモジュールが検索されます。つまり、express という名前のコアモジュールが提供されるようになったら、npm install でインストールされた express の方は参照されなくなるということですね。格差社会。。。
require のパラメータが './name'、'../name'、'/name' というように、./ や / で始まるパスで指定された場合は、そのファイルが置かれたディレクトリからの相対パス(あるいは絶体パス)で、下記のようにファイルが検索されます。
name という名前のファイルを探す。name.js という名前のファイルを探す。name.json という名前のファイルを探す(JSON ファイルとしてロード)。name.node という名前のファイルを探す(バイナリ addon として dlopen でロード)。name/package.json というファイルがあれば、その main フィールドを見て、name/<mainフィールドの値> というパスを使用して 1 からやり直し。main フィールドの記載がなければ、下記のステップへ続く。name/index.js という名前のファイルを探す。name/index.json という名前のファイルを探す(JSON ファイルとしてロード)。name/index.node という名前のファイルを探す(バイナリ addon として dlopen でロード)。例えば、/aaa/bbb/main.js 内で require('./mylib') とすると、下記のように検索されていきます。
/aaa/bbb/mylib という JavaScript ファイル/aaa/bbb/mylib.js という JavaScript ファイル/aaa/bbb/mylib.json という JSON ファイル/aaa/bbb/mylib.node というバイナリ addon ファイル/aaa/bbb/mylib/package.json に name フィールドがあれば、mylib/<mainフィールドの値> が指定されたものとして同様に検索/aaa/bbb/mylib/index.js という JavaScript ファイル/aaa/bbb/mylib/index.json という JSON ファイル/aaa/bbb/mylib/index.node というバイナリ addon ファイルちなみに、require に渡す名前に ./sample.js のように、拡張子まで含めて指定した場合も、上記のアルゴリズムはそのまま実行されます。
.js という拡張子を持つファイルだけが検索されるわけではありません。
つまり、sample.js というファイルが見つからなければ、sample.js.js や sample.js.json のようなファイル名で検索が行われます。
Node は上記で説明したパスからだけでなく、下記のようなディレクトリからもモジュールを検索します(参考: Loading from the global folders) 。
NODE_PATH 環境変数に列挙されたディレクトリ(Linux はコロン区切り、Windows はセミコロン区切りで列挙)$HOME/.node_modules$HOME/.node_libraries$PREFIX/lib/nodeただし、これらの仕組みは現在のような高度なモジュール検索アルゴリズムが導入される前に作られたもので、互換性のために残されています。 環境毎の設定の違いにより、アプリケーションの振る舞いが大きく変わってしまう可能性があるので、できるだけグローバルフォルダの仕組みは使わないようにしましょう。
Node.js は、require による同一モジュールの読み込みを効率化するため、読み込んだモジュールをキャッシュしています。
主にメモリ効率や速度向上のための仕組みですが、副次的な作用として、モジュールインスタンスが複数のクライアントモジュール間で共有されることに注意してください。
次のサンプルコードの動作を見るとわかりやすいです。
exports.value = 100;
const mylib = require('./mylib');
mylib.value = 200;
const mylib = require('./mylib');
console.log(mylib.value);
const stranger = require('./stranger');
console.log(mylib.value);
$ node main.js
100
200 ★mylib.value の値が書き換わっている
mylib モジュールは、value 変数を公開しています。
mylib.value の値は最初 100 ですが、stranger.js の中で間接的に 200 に書き換えられています。
stranger.js から読み込んだ mylib モジュールインスタンスは、main.js が参照しているインスタンスと同じものなので、stranger.js で mylib インスタンスの内容を書き換えると、その影響が main.js 側の mylib インスタンスにも及ぶことになります(stranger.js をロードするだけで mylib.value の値が 100 から 200 に変わってしまう)。
このキャッシュの仕組みを、モジュール間の値のやりとりに利用できそうだと思うかもしれませんが、そのような用途で使うのは避けるべきです。
なぜなら、同一だと思われるモジュールでも別のキャッシュインスタンスが生成されることがあるからです。
Node.js は、キャッシュされたモジュールの識別子として、require のパラメータで指定された名前を使用しています。
ロードするモジュールの相対的なパスが変わったり、ファイル名の指定方法が変わるだけで別のインスタンスが生成される可能性があります。
例えば、Windows のファイルシステムは大文字と小文字を区別しませんが、Node.js がモジュールインスタンスをキャッシュするときは、大文字と小文字が区別された名前を識別子として使用します。
Windows 環境において、mylib.js と MYLIB.js は同一のファイルを示しますが、Node.js で require('./mylib') とした場合と、require('./MYLIB') とした場合は別々のモジュールインスタンスとしてキャッシュされることになります。
下記の例を見てください。
前述の main.js をちょっとだけ書き換えて、require パラメータで指定するモジュールのパスを大文字に変更しています。
const mylib = require('./MYLIB'); // 大文字で指定
console.log(mylib.value);
const stranger = require('./stranger');
console.log(mylib.value);
$ node main.js
100
100 ★stranger.js の中の変更は main.js に影響しない
このようにすると、main.js の中の mylib インスタンスと、stranger.js の中の mylib インスタンスは別々のインスタンスとして扱われるため、stranger.js の中での mylib インスタンスの変更は main.js 側に影響しません。