node環境には、導入しなくても使用でき、他のどこにもない組み込みのグローバル変数が2つあります:moduleとrequireで、nodejsモジュールシステムを構成しています。
const fs = require('fs')
const add = (x, y) => x + y
module.exports = add
通常の使い方では、モジュールのインポートとエクスポートを行うだけですが、少し掘り下げてみると、それだけではありません。この業界では、これらを使ってできるトリッキーなことがいくつかあります。これらのハックを使うことはお勧めしませんが、それでも、これらについて少し知っておくことは必要です。
- アプリを再起動せずにモジュールをホットロードするには?jsonファイルを要求するとキャッシュが作成されますが、ファイルを書き換える場合、どのようにそれを見るのですか?
- exports
- require
module wrapper
nodeでモジュールを書くとき、モジュールは実際には次のように関数でラップされます。
(function(exports, require, module, __filename, __dirname) {
// すべてのモジュール・コードはこの関数でラップされる
const fs = require('fs')
const add = (x, y) => x + y
module.exports = add
});
- module
- __filename
- __dirname
- ファイル名
- __ディレクトリ名
module
このモジュールが何なのか知りたいのであれば、それを印刷してください!
const fs = require('fs')
const add = (x, y) => x + y
module.exports = add
console.log(module)
- exports: 実はmodule.exports
- require: ほとんどの場合
Module.prototype.require
module.exports vs exports
- exports: 実際には module.exports への参照。
- __filename
- モジュール
- ファイル名
- __dirname。
path.dirname(__filename)
// <node_internals>/internal/modules/cjs/loader.js:1138
Module.prototype._compile = function(content, filename) {
// ...
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
// ここからわかるように、exports= module.exports
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new Map();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}
// ...
}
require
nodeのREPLコンソールやVSCodeでrequireをエクスポートしてデバッグすると、requireが非常に複雑なオブジェクトであることがわかります。
// <node_internals>/internal/modules/cjs/helpers.js:33
function makeRequireFunction(mod, redirects) {
const Module = mod.constructor;
let require;
if (redirects) {
// ...
} else {
// require 実際にはモジュール.prototype.require
require = function require(path) {
return mod.require(path);
};
}
function resolve(request, options) { // ... }
require.resolve = resolve;
function paths(request) {
validateString(request, 'request');
return Module._resolveLookupPaths(request, mod);
}
resolve.paths = paths;
require.main = process.mainModule;
// Enable support to add extra extension types.
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
require(id)
require関数はモジュールの導入に使われ、最も一般的でよく使われる関数です。
// <node_internals>/internal/modules/cjs/loader.js:1019
Module.prototype.require = function(id) {
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
requireDepth++;
try {
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
}
requireがモジュールを導入するとき、実際にロードされるのはModule._loadで、大まかにまとめると以下のようになります:
- Module._cacheがモジュールキャッシュにヒットした場合、module.exportsが直接取り出され、ロードが終了します。
- require 実はmodule.require
// <node_internals>/internal/modules/cjs/loader.js:879
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
// ...
}
const filename = Module._resolveFilename(request, parent, isMain);
const cachedModule = Module._cache[filename];
// キャッシュにぶつかったら、キャッシュをフェッチするだけだ
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// NativeModuleならロードしろ。
const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// Don't call updateChildren(), Module constructor already does.
const module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
if (parent !== undefined) { // ... }
let threw = true;
try {
if (enableSourceMaps) {
try {
// NativeModuleでない場合はロードすること!
module.load(filename);
} catch (err) {
rekeySourceMap(Module._cache[filename], err);
throw err; /* node-do-not-add-exception-line */
}
} else {
module.load(filename);
}
threw = false;
} finally {
// ...
}
return module.exports;
};
require.cache
コードがrequire(lib)を実行すると、libモジュールの内容がキャッシュされたコピーとして実行され、次に参照されたときにはモジュールの内容は実行されません。
この場合のキャッシュはrequire.cacheで、前の段落で言及したModule._cacheです。
// <node_internals>/internal/modules/cjs/loader.js:899
require.cache = Module._cache;
ちょっとしたテストです。
index.jsとutils.jsの2つのファイルがあります。utils.jsにはprintオペレーションがあり、index.jsがutils.jsを複数回参照すると、utils.jsのprintオペレーションが複数回実行されます。コード例を以下に示します。
インデックス
// index.js
// これは2回引用されている
require('./utils')
require('./utils')
ユーティリティ
// utils.js
console.log('一度実行されると')
つまり、index.jsの最後にrequireを出力するrequire.cacheは、モジュール・キャッシュを見つけます。
// index.js
require('./utils')
require('./utils')
console.log(require)
そこで冒頭の質問に戻ります:
アプリケーションを再起動せずにモジュールをホットロードするには?
A:Module._cacheの削除は、 メモリリークを引き起こしたrequire.cacheの1行削除のような問題を引き起こす可能性があります 。
だからそれは、この黒魔術が大幅に物事を再生する開発環境のコアコードを変更することができますが、本番環境に行くに実行されない、結局のところ、黒魔術は制御できません。