blog

Nodeでモジュールを導入する方法とその詳細

node環境では、導入されなくても使える2つの組み込みグローバル変数があり、それらはどこにでもあります:moduleとrequireです。 以下は簡単な例です。 通常の使い方ではモジュールの導入とエク...

May 31, 2020 · 6 min. read
シェア

node環境には、導入しなくても使用でき、他のどこにもない組み込みのグローバル変数が2つあります:moduleとrequireで、nodejsモジュールシステムを構成しています。

const fs = require('fs')
const add = (x, y) => x + y
module.exports = add

通常の使い方では、モジュールのインポートとエクスポートを行うだけですが、少し掘り下げてみると、それだけではありません。この業界では、これらを使ってできるトリッキーなことがいくつかあります。これらのハックを使うことはお勧めしませんが、それでも、これらについて少し知っておくことは必要です。

  1. アプリを再起動せずにモジュールをホットロードするには?jsonファイルを要求するとキャッシュが作成されますが、ファイルを書き換える場合、どのようにそれを見るのですか?
  2. exports
  3. 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

module.exportsと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で、大まかにまとめると以下のようになります:

  1. Module._cacheがモジュールキャッシュにヒットした場合、module.exportsが直接取り出され、ロードが終了します。
  2. 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行削除のような問題を引き起こす可能性があります 。

だからそれは、この黒魔術が大幅に物事を再生する開発環境のコアコードを変更することができますが、本番環境に行くに実行されない、結局のところ、黒魔術は制御できません。

Read next

Feign拡張パッケージをジャークした

最近、同社は、Javaを使用してコア業務システムをリファクタリングする準備ができている、半年はJavaを書かなかった、JDKはJDK11と他の問題の最終的な選択の安定性を考慮して、14に更新されました。 全体的なアーキテクチャの選択では、それがブランドの新しいシステムであるという事実のために、手荷物の歴史はありませんし、同時にチームが町に座って雄牛の数を持っているので、オプションが大胆になります。 最終的な結果は...

May 31, 2020 · 5 min read