Node.jsのexportsとmodule.exportsのメモ

はじめに

nodejsのモジュール機構がよくわからない。ES6以降だとJavaScriptにモジュール機構の仕様(import)があるが、node.jsで使われるCommonJS(exportsとかrequireとか)の動きがよくわからん。

ということで、メモ。

参考になる記事は沢山あり。以下記事の動作を確認しながらメモをしていく。

一つ目

www.webdesignleaves.com

2つ目

jovi0608.hatenablog.com

まずは、1つ目から。

Node.jsのexportsとmodule.exports

  • Node.jsではCommonJS(CJK)フォーマットが使われる
  • モジュールとその依存ファイルの定義には以下が使われる
    • require
    • exports
    • module.exports
  • 参考Understanding module.exports and exports in Node.js
    • モジュールフォーマット?には以下がある
      • Asynchronus Module Definition(AMD)
      • CommonJS(CJS)
      • ES Module(ESM)
      • System.register(なんだこれ?)
      • Universal Module Definition(UMD)
        • ブラウザとNode.js両方で使えるフォーマット

require

  • モジュールやファイルをインポートする関数
    • 引数
      • モジュール(ファイル)のパス
      • node_moduleディレクトリにあるモジュールの名前
      • ビルトインモジュールの名前
    • 拡張子は省略可能(ESMでは省略不可らしい)
  • エクスポートされたモジュールを戻り値として返す
    • 変数に代入してその変数を使うことでモジュールを扱える
//// file1.js
module.exports = "hoge";

//// main.js
// ローカルファイルfile1.jsをインポート
const hoge = require("./file1");
console.log(hoge) ; // hoge

// node_modulesディレクトリにあるbarをインポート
const bar = require("bar");
// node_modules/barディレクトリを作って、
// npm init -y でpacakge.jsonを作って、
// mainに指定されているindex.jsに「module.exports = "bar"」を記載
// これで、モジュールとしてbarが読めた
// これが最小構成なのかも

// ビルトインモジュールfsをインポート
const fs = rquire("fs");

モジュールの検索パス

$ node -e "console.log(module.paths)"
[
  '/home/xxxx/git/cjs_esm/node_modules',
  '/home/xxxx/git/node_modules',
  '/home/xxxx/node_modules',
  '/home/node_modules',
  '/node_modules'
]

ビルトインモジュール

 node -e "console.log(require('module').builtinModules
)"
[
  '_http_agent',         '_http_client',        '_http_common',
  '_http_incoming',      '_http_outgoing',      '_http_server',
  '_stream_duplex',      '_stream_passthrough', '_stream_readable',
  '_stream_transform',   '_stream_wrap',        '_stream_writable',
  '_tls_common',         '_tls_wrap',           'assert',
  'assert/strict',       'async_hooks',         'buffer',
  'child_process',       'cluster',             'console',
  'constants',           'crypto',              'dgram',
  'diagnostics_channel', 'dns',                 'dns/promises',
  'domain',              'events',              'fs',
  'fs/promises',         'http',                'http2',
  'https',               'inspector',           'module',
  'net',                 'os',                  'path',
  'path/posix',          'path/win32',          'perf_hooks',
  'process',             'punycode',            'querystring',
  'readline',            'repl',                'stream',
  'stream/consumers',    'stream/promises',     'stream/web',
  'string_decoder',      'sys',                 'timers',
  'timers/promises',     'tls',                 'trace_events',
  'tty',                 'url',                 'util',
  'util/types',          'v8',                  'vm',
  'worker_threads',      'zlib'
]

moduleというグローバル変数もあるけど、require('module')とは別なのか。

exports

  • モジュールを他のプログラムで使用できるようにすには以下を使う
    • exports
    • module.exports
//user.js
exports.getName = () => { return "hoge" }

// main.js
const user = require('./user.js');
console.log(user.getName());

分割代入

const { getName, dob } = require('./user');

module.exports

  • 1つだけをエクスポートするモジュールがある場合などはmodule.exportsを使用するのが一般的
class User { xxxx }
module.exports = User;

exportsとmodule.exports

  • exportsはmodule.exportsのショートカットのようなもの
  • 初期状態ではexportsはmodule.exportsへの参照
exports.a = "A";
exports.b = "B";
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports); // true

exportsとmodule.exportsが異なる場合

  • exports( or module.exports) の参照を上書きしてしまうと異なるものを指す
  • 参照上書きしてしまうと、module.exportsの方が有効になるぽいな
exports = { hoge: "hoge" };
console.log(exports === module.exports); // false

次は2つ目の記事を読みます

Node.js : exports と module.exports の違い(解説編)

moduleとexports

  • module
    • 現在のモジュールへの参照
    • module.exportsはexportsオブジェクトと同じ
    • moduleは実際はグルーバルではなくて各モジュール毎のローカル
  • exports
    • 現在のモジュールのすべてのインスタンス間で共有されるオブジェクト
      • exportsオブジェクトはシングルトン?
      • 複数のファイルからrequireしてたら、同じオブジェクトを参照している?
        • 実験してみたら、複数ファイルからrequireしたら同じインスタンスだった
    • requireを通じてアクセス可能になる
    • exportsはmodule.exportsと同じオブジェクト
  • module.exportsが本体。exportsはlittle helperらしい。
    • だから、module.exports = { xxx } みたいに参照を更新したら、exportsは使えないのか。

使い分け

  • モジュールを特定のオブジェクト型にしたいならmodule.exportsを使う
    • module.exports = User みたいな。
    • require()の戻り値を、コンストラクター関数や配列・文字列などにしたい場合
  • 通常のmoduleインスタンスとして使うなら、exportsを使う
    • const obj = require("./myobj"); console.log(obj.hoge)みたいな。

src/node.jsを読む

  • `NativeModule.wrapという処理で、モジュール内のソースを無名関数でラップしている
    • exports, require, module,filename, dirnameはその無名関数の引数
    • いまはESM対応もされているしこのあたりのロジックは結構変わってるのかな
(function(exports, require, module, __filename, __dirname) {
  モジュールファイル内の JavaScript コード
});

最新っぽいソース

  • bootstrap/loader.jsとmodues/cjs(or esm)/loader.jsになってる

node/loaders.js at master · nodejs/node · GitHub

node/loader.js at master · nodejs/node · GitHub

記事の擬似コードがとても参考になる。

  • 本体のコード(requireが記載されているコード)より先に、moduleが読み込まれる。
  • moduleオブジェクトはrequireされる毎にインスタンス化される
  • requireからの戻り値はmodule.exports変数。

基本的には、module.exportsが本体なのでこっちが優勢。exportsは参照が上書きされることがある。

ただし、module = { exports : { xxx} } みたいにすると、moduleはrequire毎にインスタンス化されるので、exportsの方が優勢になる。

よくわかりました。

次は、ESMの方もキャッチアップしたい。