requireの仕組み
こんばんは
この記事は Node.js Advent Calender 2014の23日目の記事です。
Node.js Advent Calendar 2014 - Qiita
普段node書くとき、何気なく使ってるrequireだけど、どんな風にモジュールが読み込まれてるのかコアコードの中を追ってみる。
https://github.com/joyent/node/blob/v0.11.14/lib/module.js#L362
Module.prototype.require = function(path) { assert(util.isString(path), 'path must be a string'); assert(path, 'missing path'); return Module._load(path, this); };
こいつが各moduleが読み込まれた時にセットされるrequireの本体。
簡単なパラメータチェックをしてModule._loadに処理を渡している。
この時はまだpathのresolveなどはまだしていない。 単純な移譲。
module._load
Module._load = function(request, parent, isMain) { // ...省略 // [A] var filename = Module._resolveFilename(request, parent); // [B] var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; } // [C] if (NativeModule.exists(filename)) { //...省略 return NativeModule.require(filename); } // [D] var module = new Module(filename, parent); // ...省略 Module._cache[filename] = module; // [E] var hadException = true; try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } return module.exports; };
[A]
ここで初めてファイル名の解決が行われます。
Module._resolveFilename
というAPIを使ってパスを解決しています。
このAPIは第二引数のparent
(requireを実行しているmodule自身)のパスから相対的に解決していきます(native moduleを除く)
[B]
ファイルのパスが先ほどの手順で解決されているのでこれをキーとして、すでにそのモジュールが読み込まれていてキャッシュが存在していればそれのexports
を返すというような実装になってます。
Module._cache
はfile名をキー、値にはmodule自身をつっこんでるキャッシュ用のオブジェクトです。
ここで注意したいのは返してるのはmodule.exports
の値だけです。
通常ロードされたモジュールに展開されるローカル変数module
はその呼び出し元moduleのparent
プロパティ(
http://nodejs.jp/nodejs.org_ja/api/modules.html#modules_module_parent)をもってるはずですが、この辺はrequireの度に設定されなおしたりはしません。
なのでmodule.parent
をたよりにした実装してるとこの辺でいつか死ぬのでやめといたほうがいい。
以前死にました。( module.parent.filename is cached. · Issue #6149 · joyent/node · GitHub )
[C]
ロード対象にされてるモジュールがnativeなモジュール(pathとかfsとかそういうやつ)かどうかを判定し、そうであればnativeモジュール用のローダーで読み込みます。 (https://github.com/joyent/node/blob/v0.11.14/src/node.js#L783)
[D]
ここで新たなモジュールとしてModuleのインスタンスを作ります。(ここで渡されてるparent
がmodule.parent
として永久に保持される)
そしてそれをそのままパスをキーとしてキャッシュします。
この段階では別にパスからソースをコンパイルしたりはしてません。
とくにコンストラクタ内にそういう処理はありません。
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; }
[E]
ここでModule.load
を利用してソースをコンパイルし、module.exports
を返却してます。
この時読み込みに失敗したエラーはcatchはされないものの、その後のfinalyでcacheだけは綺麗に消されるので、ロード失敗時に変なキャッシュが残ることはないはずです。
次はソースをコンパイルするところを追います。
module.load
Module.prototype.load = function(filename) { // 省略 // [A] var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; // [B] Module._extensions[extension](this, filename); this.loaded = true; };
[A]
拡張子ごとのローダーを利用するため、どのローダーを利用するかの判別のためにファイルパスから拡張子を抜き出します。 (デフォルト .js)
ローダーがないような拡張子の場合はとりあえず.js用のローダーで試すみたいです。
[B]
拡張子ごとのローダーによって読み込みを開始します。
Module.exteisons[extension]
というのがローダーです。
通常我々が利用するのは.js
と.json
くらいでしょうか。 あと一応.node
というローダーもあるみたいです。
// Native extension for .js Module._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(stripBOM(content), filename); }; // Native extension for .json Module._extensions['.json'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); try { module.exports = JSON.parse(stripBOM(content)); } catch (err) { err.message = filename + ': ' + err.message; throw err; } };
.jsonのほうは簡単ですね。
ファイルを同期的に読み出してstripBOM
でutf-8のBOM(http://www.wdic.org/w/WDIC/UTF-8#BOM)を取り除いたものをJSON.parse
してオブジェクトにもどしてそれをmodule.exports
にセットしています。
.jsonはこれでおしまいです。
.jsのほうはもうちょっと複雑なのでBOMを排除したファイルコンテンツを取得したあと、module._compile
に処理を移譲しています。
よくこのローダーはテストとかでrequireしたものをスタブに置き換えるために使われたりします。
ドキュメント上では廃止予定となってるけどコアコードに根深く存在してるので事実上これは廃止できませんみたいなこと書いてあるので複雑な感じですね。
でも、そういうことしたいとき、多分ほかの方法はvm使ったりとかもっと荒々しい方法とかになると思います。
module._compile
https://github.com/joyent/node/blob/v0.11.14/lib/module.js#L378
長かったけどこれで最後です。
Module.prototype._compile = function(content, filename) { var self = this; // [A] // remove shebang content = content.replace(/^\#\!.*/, ''); // [B] function require(path) { return self.require(path); } require.resolve = function(request) { return Module._resolveFilename(request, self); }; Object.defineProperty(require, 'paths', { get: function() { throw new Error('require.paths is removed. Use ' + 'node_modules folders, or the NODE_PATH ' + 'environment variable instead.'); }}); require.main = process.mainModule; // Enable support to add extra extension types require.extensions = Module._extensions; require.registerExtension = function() { throw new Error('require.registerExtension() removed. Use ' + 'require.extensions instead.'); }; require.cache = Module._cache; // [C] var dirname = path.dirname(filename); // 省略 // create wrapper function // [D] var wrapper = Module.wrap(content); var compiledWrapper = runInThisContext(wrapper, { filename: filename }); // 省略 // [E] var args = [self.exports, require, self, filename, dirname]; return compiledWrapper.apply(self.exports, args); };
[A]
ソースからシェバン( #!/usr/bin/env node ←こういうの)があれば取り除きます
[B]
読み込まれるモジュールのローカル変数として使うrequire
を定義します。
なかみは単純にmodule.require
です。
ここまででみてきたrequire
と同じものです(属するmoduleは呼び元と呼び先とで違うけど)
require
のもつメソッド( http://nodejs.jp/nodejs.org_ja/api/globals#globals_require )を定義していきます。
require.resolve
は中でModule._resolveFilename
に処理を移譲してますね。
これはmodule._load
の中でファイルパスを解決したメソッドです。
なのでrequire.resolve
を使ってファイルパスを解決すればそのままモジュールのキャッシュキーが安全につくれたりします。
あとはちらほら廃止になったapi用の対応がみられますね。
[C]
この読み込まれるモジュールのディレクトリパスを取得しています。
これが読み込まれるモジュールのローカル変数として使う__dirname
になります。
ちなみに__filename
はさきほどModule._resolveFilenameによって解決されたパスをそのままつかいます。
[D]
ソースをラップします。
以前この部分だけ記事にしました( Nodeのファイルスコープ - ぶれすとつーる )
実行コード(文字列)を
'(function (exports, require, module, __filename, __dirname) { ' + source + '\n});'
こんな感じでラップするものです。
こうすることで読み込まれるモジュールにスコープができ、あらかじめそこに存在するローカル変数を(引数にセットすることで)用意することができます。
そしてこうしてできたコード文字列をrunInThisContext
でコンパイルします。
runInThisContext
はvmモジュールのvm.runInThisContext
と同じです。
var runInThisContext = require('vm').runInThisContext;
これは実行元のローカル変数とかにはアクセスできないけど同じglobalを共有形式のコード評価です。
第二引数で渡してるfilenameのオプションはスタックトレース時の表示用の情報です。
これで function (exports, require, module, __filename, __dirname { [source] }
が得られました。
[E]
あとはそれぞれ引数( self.exports, require, self, filename, dirname
)をapplyでセットして実行しています。
self
は自身のmodule
をさします。
module.exports
とexports
が同じ参照のものだということがここからわかりますね。
しばしばモジュール内で
exports = function () { ... }
が期待した動きをしないけどなんで??みたいな質問がwebに溢れてますがこれをみれば一目瞭然ですね。
ただのローカル変数なんだから参照を切るような代入をしたらmodule.exportsに反映されなくなりますね、exportsにはなんのマジック的要素もありません。
module.exports = function () { ... }
を使いましょう。
長くなりましたが各モジュールのrequire
の動きをおってみました。
途中横道にそれそうな処理は省略しましたがnative_moduleの解釈の仕方などもあるので見ると収穫があるかもしれません。