📌

『ハンズオンNode.js』ESモジュールまとめ

2023/08/23に公開

ESモジュールの構文

exportとimport

  • 拡張子: .mjs
  • 常にstrictモードが適用されるため、ファイルの先頭に'use strict'と記述する必要がない
  • REPLではimport文でESモジュールをインポートできない。
    • フラグやpackage.jsonの内容に関係なく。
  • ファイルを作成せずにimportの挙動を確認する方法
    • -eオプションでコードを文字列で指定して実行する
      $ node --input-type=module -e "import './esm-math.mjs'"
      
    • 前のコマンドの実行結果をパイプしてnodeコマンドに渡す
      echo "import { add } from './esm-math.mjs'; console.log(add(1, 2))" | \
      node --input-type=module
      

CommonJSモジュールにおけるモジュールスコープ変数に相当するもの

  • CommonJSモジュールにあってESモジュールにない変数
    • require
    • module
    • __filename
    • __dirname
    • これらを参照しようとするとエラーになる。
  • ESモジュールでこれらに相当する情報を取得する方法
    import { fileURLToPath } from 'url'
    import { dirname } from 'path'
    import { createRequire } from 'module'
    
    const __filename = fileURLToPath(import.meta.url)
    const __dirname = dirname(__filename)
    const require = createRequire(import.meta.url)
    

CommonJSモジュールとESモジュールの識別

  • あるファイルが、CommonJSモジュール or ESモジュールかの判定
  • CommonJSモジュールに判定されるケース
    • 拡張子が.cjs
    • 一番近い階層のpackage.json"type"フィールドの値が指定されていない、または"commonjs"
  • ESモジュールに判定されるケース
    • 拡張子が.mjs
    • 一番近い階層のpackage.json"type"フィールドの値が"module"

CommonJSモジュールとESモジュールの性質の違い

  • ESモジュールは静的だが、CommonJSモジュールは動的なため、静的解析できない。
    • 静的解析は、何を解析するのか?
      • あるモジュールが、他のどのモジュールに依存し何を公開するか
    • CommonJSモジュールのスコープの変数は、ただのJavaScriptオブジェクトまたは関数である。
      • 特に制約がないため、トップレベルでなくif文/for分/三項演算子/関数の中などに書くことも可能。
      • 以下のようなrequire文やexport文があった場合、モジュールを実際に実行してみないと「あるCommonJSモジュールが、他のどのモジュールに依存し何を公開するか」を知ることができない。
        // 動的なrequire()
        // Math.random()は0以上1未満のランダムな数値を返す
        const fooOrBar = require(Math.random() < 0.5 ? 'foo' : 'bar)
        
        // 動的なexports()
        for (const name of ['foo', 'bar']) {
          exports[name] = name
        }
        
  • ESモジュールは静的なため、モジュールのパースの段階で依存関係の解析が可能。
    • import文やexport文は、ESモジュールのために用意された特別な構文である。
      • 常にモジュールのトップレベルに記述必要があるため、if文/for分/三項演算子/関数の中などに書くと構文エラーになる。
  • 静的であることの利点
    • モジュールのパースの段階で依存関係を解析できる。
      • 依存関係に不備がある時は、コードを実行することなくNode.jsのプロセスが異常終了する。
      • 上記により、コードの不具合にいち早く気づくことができる。
  • ESモジュールではimport文が巻き上げ(hoisting)られる。
    • import文がどこに書いてあってもモジュール内で最初に実行される。

CommonJSモジュールとESモジュールの相互依存

  • ESモジュールにおけるCommonJSモジュールのimport
    • 依存先のCommonJSが何を公開しているのかパースの時点で知っておく必要がある。可能な限りでそれを満たすためにNode.jsは次の解決策をとっている。

      • CommonJSのデフォルトインポートは常に可能。
      • 静的解析の時点で、公開しているとみなされた値は名前付きインポートが可能。
    • 静的解析で検出されないexportの例

      // cjs.jsファイルの想定
      const c = 'c'
      exports[c] = c
      
      node --input-type=module -e "import { c } from './cjs.js'; console.log(c)"
      
      • 変数によってexportsに割り当てられたcは検出されず、名前付きインポートは構文エラーになる。
      • これは、名前付きインポートはパース時点で解決できる必要があるのに対し、cの値へのアクセスは実行時に解決されるため。
      • cにアクセスするには、以下のようにしてimportする。
        // デフォルトインポートした上で、cjs.cを用いる。
        import cjs from './cjs.js'
        
    • 今のところはexport.a = 'a'のようにシンプルに公開している値であれば問題なく名前付きインポートできることを知っておけば問題ない。

二重パッケージとpackage.jsonのexportsフィールド

  • 二重パッケージとは
    • CommonJSモジュールとESモジュールの両方を含むnpmパッケージのこと。
    • npmパッケージはESモジュールとして公開することも可能だが、CommonJSモジュールとしての利用も合わせてサポートされているケースも多い。
  • package.jsonのexportsフィールドで条件付きexportsという機能を使えば、1つのパスでCommonJSモジュールもESモジュールもロード可能になる。
    {
      "exports": {
        "./sub": {
          "require": "./lib/sub.cjs",
          "default": "./lib/sub.mjs",
        }
      }
    }
    

参考文献

この記事は以下の情報を参考にして執筆しました。

https://www.oreilly.co.jp/books/9784873119236/


オプティマインドでは「多様性が進んだ世の中でも、全ての人に物が届く世界を持続可能にする」という物流業界の壮大な社会課題を解決すべく、 一緒に働く仲間を大募集中です。 少しでも興味が湧いた方は是非お気軽にカジュアル面談をお申し込みください!

https://recruit.optimind.tech/

Discussion