CodeMafiaさんのNode.jsで学ぶWebシステムとソフトウェア開発基礎!Node.js完全入門ガイド
に入門したので忘れそーと思ったことなどを備忘録としてまとめました。
若干自分の言葉に変換してたり解釈が誤ってる可能性もあります。また、それは知ってるわーてことなんかは端折ったりもしていますので、前提がなくて分からないとか、そもそも信用ならないという方はぜひ受講をおすすめします。
CodeMafiaさんの講義は色々購入させていただいているのですがどの講義も他の方だとさらっと飛ばされるような所に一歩踏み込んで裏側の仕組みまで説明してくださる場面が多いのでめっちゃ面白くておすすめです。
JavaScriptでのモジュールシステムについて
歴史的な関係もありCommonJSとESModuleの2つが存在している。
CommonJSの特徴
- サーバー側(NodeJS)独自のモジュールシステムなのでフロント側では使用不可
- 使用するためのキーワードは主に以下の3つ
-
require('ファイルパス')
: 使う側(ファイルパスの拡張子は省略可能) -
module.exports
: 使われる側(「=」は必要) -
exports
: 使われる側(「=」は必要)
-
以下に具体例を記載します。
その前に!!!
任意で良い部分についてはaaa, bbb, ccc, ...のように同じアルファベットを3回連続記載しており、
使われる側つまり公開したい定義を持っている側のファイル名末尾にLib、そして使う側のファイル名末尾にClientとつけておこうと思います。
module.exports / require
基本形
function hello() { return 'helloメソッドです。' }
module.exports = hello // hello()を公開するという意味
const bbb = require('./sampleLib') // 公開された定義をbbbという名前で使用するという意味
console.log(bbb())
# aaaディレクトリで以下実行
$ node sampleClient.js
helloメソッドです。
複数Verその1
function hello() { return 'helloメソッドです。' }
function bye() { return 'byeメソッドです。' }
module.exports = {
hello, // hello()をオブジェクトのhelloというプロパティ名で公開するという意味
bye // bye()をオブジェクトのbyeというプロパティ名で公開するという意味
}
const bbb = require('./sampleLib') // 公開された定義をbbbという名前で使用するという意味
- console.log(bbb())
+ console.log(bbb.hello())
+ console.log(bbb.bye())
# aaaディレクトリで以下実行
$ node sampleClient.js
helloメソッドです。
byeメソッドです。
複数Verその2
function hello() { return 'helloメソッドです。' }
function bye() { return 'byeメソッドです。' }
module.exports = {
hello,
bye
}
- const bbb = require('./sampleLib')
+ const { hello, bye } = require('./sampleLib') // 使われる側のプロパティに合わせて分割代入
- console.log(bbb.hello())
- console.log(bbb.bye())
+ console.log(hello())
+ console.log(bye())
# aaaディレクトリで以下実行
$ node sampleClient.js
helloメソッドです。
byeメソッドです。
exports / require
基本形
function hello() { return 'helloメソッドです。' }
exports.bbb = hello // bbbというプロパティにhello()を追加して公開するという意味
const ccc = require('./sampleLib') // 公開された定義をcccという名前で受取り
console.log(ccc.bbb()) // bbbというプロパティにhello()が追加されているのでbbb()でhello()が呼び出せる
# aaaディレクトリで以下実行
$ node sampleClient.js
helloメソッドです。
複数Verその1
function hello() { return 'helloメソッドです。' }
+ function bye() { return 'byeメソッドです。' }
exports.bbb = hello
+ exports.ddd = bye
const ccc = require('./sampleLib')
console.log(ccc.bbb())
+ console.log(ccc.ddd())
# aaaディレクトリで以下実行
$ node sampleClient.js
helloメソッドです。
byeメソッドです。
複数Verその2
function hello() { return 'helloメソッドです。' }
function bye() { return 'byeメソッドです。' }
- exports.bbb = hello
- exports.ddd = bye
+ exports.bbb = {
+ hello,
+ bye
+ }
const ccc = require('./sampleLib')
- console.log(ccc.bbb())
- console.log(ccc.ddd())
+ console.log(ccc.bbb.hello())
+ console.log(ccc.bbb.bye())
# aaaディレクトリで以下実行
$ node sampleClient.js
helloメソッドです。
byeメソッドです。
おまけ
大きくmodule.exports / require
と exports / require
の2通りの方法があることが分かりました。
そして実はmodule.exportsとexportsは同じ参照先を指しているそうです。
そちらについては以下を実施してみることで確認することが出来ます。
console.log(module.exports === exports)
# aaaディレクトリで以下実行
$ node sampleClient.js
true
つまりmodule.exports = { 何かしらの関数名 }
は直で公開する関数を格納しており
exports.bbb = 何かしらの関数名
はmodule.exportsのプロパティ名bbbに公開する関数を追加で格納しているということが分かります。まぁ使う側の場合はconst aaa = require(ファイルまでのパス)
って書けばある程度エディタの補完が効いて教えてくれるので問題ないと思います。
一方で使われる側の視点から見るとexports
はmodule.exports
の参照先を持っているから短く書けるというだけで単にmodule.
を省略したら良いというわけではありません。
exports = module.exports
されていると考えてあくまでmodule.exports
を軸に覚えたほうが良いですね。
使われる側で気をつけないといけないのは、以下です。
console.log(exports === module.exports) // 先述の通り参照先が同じなのでtrue
// OK例(module.exports)
module.exports = {
x,
y
}
// OK例(exports)
exports.aaa = bbb // bbbという関数をmodule.exportsのaaaというプロパティに追加して公開という意味
exports.ccc = ddd // dddという関数をmodule.exportsのcccというプロパティに追加して公開という意味
// NG例 (これだと参照先が新しいオブジェクトで上書きされてしまうため)
exports = {
aaa,
bbb
}
ESModuleの特徴
- ECMAScript標準のモジュールシステム
- 使用するためのキーワードは以下の2つ
-
import {} from 'ファイルパス'
: 使う側(ファイルパスの拡張子は必須) -
export {}
: 使われる側(CommonJSと違って「=」は不要)
-
- ブラウザともに同じキーワードでで使用可能だがNode.jsでESModuleを使用する場合は以下いずれかの明示的な設定が必要
- package.jsonで
"type" : "module"
を設定 - 拡張子をmjsにリネーム (使われる側のみでもOK)
- package.jsonで
基本形
function hello() { return 'helloメソッドです。' }
export { hello } // hello()を公開するという意味
import { hello } from './sampleLib.js' // 公開されたhelloを使用するという意味
console.log(hello())
# aaaディレクトリで以下実行
$ node sampleClient.js
(node:19431) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
省略
import { hello } from './sampleLib' // 公開された定義をbbbという名前で使用するという意味
^^^^^^
SyntaxError: Cannot use import statement outside a module
at internalCompileFunction (node:internal/vm:76:18)
at wrapSafe (node:internal/modules/cjs/loader:1283:20)
at Module._compile (node:internal/modules/cjs/loader:1328:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1422:10)
at Module.load (node:internal/modules/cjs/loader:1203:32)
at Module._load (node:internal/modules/cjs/loader:1019:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:128:12)
at node:internal/main/run_main_module:28:49
Node.js v18.20.4
実はESModuleの特徴で記載したとおり上記のままではNodeJS上では動きません。
NodeJSでESModuleを利用する際には以下「1. typeがmoduleとなっているpackage.jsonを配置」または「2. 拡張子を.jsから.mjsにリネーム」のいずれかの対応が必要です。
1. typeがmoduleとなっているpackage.jsonを配置
{
"type": "module"
}
こちらに関しては追加で配置場所にも注意が必要となる。正しい仕様は不明だが以下試した結果を記載します。
# OK (同一階層の場合は問題なし)
root
└ sampleClient.js
└ sampleLib.js
└ package.json (typeがmodule)
# OK (それぞれのファイルの直近のpackage.jsonが同一のものでtypeがmoduleの場合は問題なし)
root
└ package.json (typeがmodule)
└ dir
└ sampleClient.js
└ sampleLib.js
# OK (間にディレクトリを挟んでいても1つ上と同じ理由で問題なし)
root
└ package.json (typeがmodule)
└ clientDir
| └ sampleClient.js
|
└ libDir
└ sampleLib.js
# NG (直近のpackage.jsonのtypeがcommonjsのため駄目)
root
└ package.json (typeがmodule)
└ dir
└ sampleClient.js
└ sampleLib.js
└ package.json (typeがcommonjs)
# NG (直近のpackage.jsonがそれぞれ違うpackage.jsonの場合はtypeがmoduleでも駄目)
root
└ dir
└ clientDir
| └ sampleClient.js
| └ package.json (typeがmodule)
|
└ libDir
└ sampleLib.js
└ package.json (typeがmodule)
2. 拡張子を.jsから.mjsにリネーム
この方法をとった場合は「1. typeがmoduleとなっているpackage.jsonを配置」は関係なくなる。
試したのは以下で拡張子をjsに戻すとエラーとなる。
ちなみにimport側での拡張子部分の記述修正は必要だがmjsにリネームするのは公開する側のみで問題なかった。(わざわざ複雑にするのは良くないので特段理由がなければ利用側もsampleClient.mjsにしたほうがリネームした方が良いとは思う。)
root
└ dir
└ clientDir
| └ sampleClient.js
|
└ libDir
└ sampleLib.mjs
└ package.json (typeがcommonjs) # mjsなので影響範囲外
CommonJSとESModuleの連携
CommonJS(使われる側) → ESModule(使う側)
は使用可能
ESModule(使われる側) → CommonJS(使う側)
は使用不可能
上記の通り、今後はESModule一択で使われる側も使う側も記載していけば良い。
つまり使用するキーワードはimport / export
となる。
ただし、CommonJS → CommonJS
となっているものも見かけると思うので知っておくと良いとは思います。
ESModule と CommonJS の違い
ESModuleでは
-
require
,exports
,module.exports
が読み込めない -
__filename
,__dirname
が使えない -
require
で JSON が読み込めない
__dirname, __filename のESModuleでの代替手段
あまりにオリジナリティなさすぎて丸コピになってしまうので記載は控えさせて下さい。
ESM で JSON を読み込む方法
上に同じ。