Node.js の import と require の違いと ES モジュールの使い方
JavaScript で外部モジュールや自作のモジュールを読み込む際に使用する import
と require
の違いやエラーの対応について纏めています。
やりたいこと
JavaScript で外部モジュールや自作のモジュールを読み込む際に使用する import
と require
についてその違いや、現時点でモジュールシステムがダブルスタンダードになっていることにより、発生しがちなエラー等について纏めておきたいと思います。
なぜモジュールシステムが 2 つ存在するか
少し長くなりますが JavaScript の歴史を追いながら、なぜ Node.js におけるモジュールシステムがダブルスタンダードになったかを見てみたいと思います。
JavaScript の誕生と標準仕様策定
JavaScript は以下の通り Netscape Communications によって開発され、後に Ecma International によって ECMA-262 として標準化されるようになりました。
JavaScript はネットスケープコミュニケーションズのブレンダン・アイクによって、1995 年 5 月に 10 日間で開発された。(中略) 1997 年、通信に関する標準を策定する国際団体 Ecma インターナショナルによって JavaScript の中核的な仕様が ECMAScript として標準化され、多くのウェブブラウザで利用できるようになった。
- JavaScript - Wikipedia
ECMA-262 初版の時点で、後に仕様策定され今日 ECMAScript モジュール (ES Module) と呼ばれる import
、 export
や default
は将来的な予約語とされており、当初よりモジュールシステムについて仕様策定する構想があったことがうかがえます。
FutureReservedWord :: one of
case
debugger
export
super
catch
default
extends
switch
class
do
finally
throw
const
enum
import
try
- ECMA-262, 1st edition
1999 年に第 3 版が策定された後、第 4 版が破棄されたことにより第 5 版の策定は 2009 年までかかります。ただ第 5 版の時点でもモジュールシステムについては未策定のままでした。
当時のオンブラウザの JavaScript のモジュール分けは HTML 上で必要なモジュールを <script>
タグで読み込んで使用することしかできませんでした。ただ <script>
タグで読み込むとグローバルに展開されるため名前空間の衝突があったり、この時点で JavaScript の変数宣言が再宣言可能な var
しかなかったことで意図せず上書きされたりと到底モジュールと言えた代物ではありませんでした。
CommonJS プロジェクトによるモジュール仕様の策定
さて一方で 2009 年に Kevin Dangoor により Web ブラウザ以外の実行環境における JavaScript 仕様を策定するプロジェクト CommonJS が立ち上げられます。CommonJS プロジェクト立上げを宣言した Kevin Dangoor のブログ記事の中でも他のモジュールを取り込む標準的な方法が言語として定められていないことを問題点の一つとして挙げています。
JavaScript needs a standard way to include other modules and for those modules to live in discreet namespaces. There are easy ways to do namespaces, but there’s no standard programmatic way to load a module (once!). This is really important, because server side apps can include a lot of code and will likely mix and match parts that meet those standard interfaces.
- What Server Side JavaScript needs
CommonJS のモジュールシステムの仕様は Modules/1.1.1 - CommonJS Spec Wiki に定義があり、2013 年にリリースされた Node.js v0.10 のモジュールシステムは CommonJS のモジュールシステムを実装しています。(Modules Node.js v0.10.48 Manual & Documentation)
これが今日 CommonJS モジュールと呼ばれている require
、exports
によるモジュールシステムです。
CommonJS モジュールはブラウザでは使えませんが、WebPack のようなバンドラーの登場により、ブラウザ向け JavaScript の開発においても CommonJS モジュールベースで開発するスタイルが流行り始めました。開発時は CommonJS モジュールによるモジュール分割の恩恵を受けつつ、デプロイ時はバンドラーで 1 ファイルにモジュールを纏め、ブラウザで実行可能とするものです。
標準仕様でのモジュール仕様策定
そこから遅れること 2015 年に ECMA-262 第 6 版が策定され、以下の通り本文の導入部分にも記載されていますが大幅に仕様が追加され、ついにモジュールシステムの仕様が策定されました。
The sixth edition is the most extensive update to ECMAScript since the publication of the first edition in 1997.
- ECMAScript 2015 Language Specification - ECMA-262 6th Edition
この仕様策定を受け、2017~2020 年にかけて主要ブラウザで ES Module の機能実装がなされます。また Node.js においても 2017 年リリースの v8 に実験的機能として ES Module が追加されます。ECMAScript Modules | Node.js v8.17.0 Documentation
さらに 2019 年リリースの Node.js v12 で正式実装にされ、Node.js における CommonJS モジュール と ES Module のモジュールシステムダブルスタンダードの状態に至ります。(Modules: ECMAScript modules | Node.js v12.22.12 Documentation)
上記の通り ES Module はブラウザでも実装されているため、ついに Webpack のようなサードパーティツールに頼らずともネイティブにモジュールベースでブラウザ向け JavaScript の開発が可能となりました。(可能になったというだけで Webpack や Rollup といったバンドラーを使った開発が実態にはなっていると思います)
どちらを使うべきか
Node.js においては現時点 (v22.6.0) で CommonJS モジュールがデフォルトとなっています。デフォルトという言い方が曖昧なので具体的には package.json
がないあるいは package.json
の type
フィールドがない場合、.js
ファイルは CommonJS モジュールベースとして扱われることをデフォルトとここでは表現しています。
If the nearest parent package.json lacks a
"type"
field, or contains"type": "commonjs"
,.js
files are treated as CommonJS. If the volume root is reached and nopackage.json
is found,.js
files are treated as CommonJS.
- Modules: Packages | Node.js v22.6.0 Documentation
これは長い間 CommonJS モジュールベースで Node.js のエコシステムが築かれてきたため、モジュールシステムのデフォルトの変更は影響が大きいためと考えられます。
ただ最近では既存の npm パッケージにおいても ES モジュール 対応が進んできており、CommonJS モジュールベースでないと困るケースは減ってきているように思います。(主観です)
何より ES モジュールは JavaScript 標準仕様で策定されているもので、今後 ES モジュールへの統一はさらに進んでいくものと想像されます。
また Node.js はブラウザ以外の JavaScript ランタイムとして広く使われた最古のものの一つで、前項で紹介したような歴史的背景でモジュールシステムがダブルスタンダードになっていますが、比較的新しいブラウザ以外の JavaScript ランタイムである Deno では最初から ES モジュールをベースにしています。
Deno by default standardizes the way modules are imported in both JavaScript and TypeScript using the ECMAScript module standard.
- ECMAScript Modules in Deno
以上のことを踏まえ、今後は ES モジュールを使っていくことがデファクトであると考えます。
Node.js で ES モジュールを使う
以下のように helloworkd
関数を名前付きエクスポートする module.js
を作成します。
名前付きエクスポート:
export
キーワードの後には、let
,const
,var
宣言や、関数、クラス宣言を使用することができます。また、export { name1, name2 }
構文を使用すると、他の場所で宣言された名前のリストをエクスポートすることができます。
- export - JavaScript | MDN
次に先ほどの module.js
から helloworld
関数を名前付きインポートして実行する index.js
を作成します。
では作成した index.js
を早速実行してみます。
すると以下のようなエラーが発生します。
前項でも触れましたがこれは Node.js のモジュールシステムは CommonJS モジュールがデフォルトになっているためです。
Node.js で ES モジュールを使用するには以下の通り拡張子を .mjs
にするか、package.json
の type
フィールドに module
を設定する必要があります。あるいは Node.js コマンドラインオプションの --input-type
で module
を指定するか、--experimental-default-type
で module
を指定する必要があります。
Authors can tell Node.js to interpret JavaScript as an ES module via the
.mjs
file extension, thepackage.json
“type” field with a value"module"
, the--input-type
flag with a value of"module"
, or the--experimental-default-type
flag with a value of"module"
. These are explicit markers of code being intended to run as an ES module.
- Modules: ECMAScript modules | Node.js v22.6.0 Documentation
それでは index.js
を index.mjs
、module.js
を module.mjs
と拡張子を .mjs
にそれぞれ変更します。実行するとエラーは出なくなります。
ES モジュールで import
する側も export
する側も拡張子を .mjs
にする必要があります。module.js
の拡張子を .mjs
にしなかった場合以下のエラーが発生します。
すべて ES モジュールを使っていて CommonJS モジュールの混在がない場合は、 package.json
で "type"="module"
を指定する方が簡単です。この場合、拡張子は js
のままでも ES モジュールとして扱われます。
他にコマンドラインオプション --experimental-default-type
に "module"
を指定することで、拡張子は js
のままでも ES モジュールとして扱われます。
コマンドラインオプション --input-type
に "module"
を指定する方法もあるようですが、REPL (Read-Eval-Print Loop) では使えないオプションなので試していません。
--input-type=type
The REPL does not support this option.
- Command-line API | Node.js v22.6.0 Documentation
TypeScript で Node.js 向け開発 (tsconfig.json
の compilerOptions.module
が node16
か nodenext
になっている) をする場合も基本的には同様で、package.json
で "type": "module"
が指定されているか、拡張しが mts
になっている場合、コンパイル後に ES モジュールベースで生成されます。
Input file name Contents Output file name Module kind Reason /package.json
{} /main.mts
/main.mjs
ESM File extension /utils.cts
/utils.cjs
CJS File extension /example.ts
/example.js
CJS No “type”: “module” in package.json
ブラウザで ES モジュールを使う
ブラウザでの ES モジュールの使い方も紹介しておきます。前項で作成した module.js
を使い回して、以下のように HTML 内の <script>
タグ内でインポートしてみます。
上記 HTML を HTTP サーバにホストしてブラウザでアクセスするとコンソールに以下のエラーが出ます。
ブラウザベースで ES モジュールを使用するには以下の通り、<script>
タグの type
属性に module
を指定する必要があります。ブラウザベースで ES モジュールを使用するにはこれが唯一の方法で、Node.js のように拡張子を .mjs
にすると ES モジュールとして扱ってくれるとかはありません。
ソースファイルの中で
import
宣言を使用するためには、ランタイムがそのファイルをモジュールと見なす必要があります。HTML では、<script>
タグにtype="module"
を加えることがこれに相当します。
import - JavaScript | MDN
<script>
タグの type
属性に module
を指定するとエラーはなくなり、コンソールに Hello
と表示されます。
なお、HTML を HTTP サーバーでホストせずに file
プロトコルで開くと、CORS に引っかかるので必ず HTTP でアクセスするようにしてください。
JSON ファイルのインポート
CommonJS モジュールの便利だったところとして JSON ファイルを JavaScript オブジェクトとしてインポートできる機能がありました。
これ地味に便利なのですが、ES モジュールでは現時点ではこのような仕様はありません。ただまさに仕様策定中で Ecma TC39 (Technical Committee 39) プロセスの Stage 3 にいるので、数年後には以下のような形で正式に使えるようになると思われます。(The TC39 Process)
Developers will then be able to import a JSON module as follows:
- GitHub - tc39/proposal-json-modules: Proposal to import JSON files as modules
なお、Node.js では実験的機能として現時点の仕様に基づいて既にこの JSON モジュールは実装されています。
適当な JSON を用意します。
JSON モジュールでインポートしてみます。
実行すると実験的機能なのでワーニングが出ますが、JSON を JavaScript オブジェクトとして扱えていることがわかります。
あとがき
Node.js に 2 種類のモジュールシステムが存在する経緯と ES モジュールの使い方について紹介しました。今後は ES モジュールがより加速していくと想像されるのでモジュールシステムの選択は慎重に行ったほうがよさそうです。