🔨

CommonJSとES Modulesについてまとめる

2022/02/27に公開1

モチベーション

普段フロントエンドを領分にしているのになかなかこのあたりの基礎が足りていないと感じることが多いので、なんとかしたい。
ES Modules方式でしか対応されていないライブラリを使おうとしてコケたので色々調べたのも含め、まとめていく。
ちなみにその辺りについてはこの神記事見ると良い。
https://blog.cybozu.io/entry/2020/10/06/170000

個人的に気になっているモジュールシステムについて掘り下げていく。

CommonJS

CommonJSとは、サーバーサイドなどのウェブブラウザ環境外におけるJavaScriptの各種仕様を定めることを目標としたプロジェクトである。

from Wikipedia

例えばNode.jsで使われている。
Node.jsはデフォルトで全てのモジュールをCommonJSで扱うが、Node.jsは最近のバージョンでES Modulesに対応するなどしていて、潮流はES Modulesに流れつつある。
https://github.com/nodejs/node-v0.x-archive/issues/5132#issuecomment-15432598
後述するが、それでもまだ捨てきれない理由がある。

モジュール

Node.jsはデフォルトで全てのモジュールをCommonJSで扱う。
https://nodejs.org/api/modules.html#modules-commonjs-modules

他モジュールを呼び出す際はrequireで呼び出す。

main.js
const functions = require('./libs/functions')
functions.hoge()

ES Modules

ECMA Script Modulesの略。

ECMAScript(エクマスクリプト)は、Ecma Internationalのもとで標準化手続きが行われているJavaScriptの規格

from wikipedia

ウェブブラウザがECMAScriptをサポートしている。

モジュール

Node.jsはデフォルトで全てのモジュールをCommonJSで扱うので、以下のいずれかの対応でモジュールシステムを変える必要がある。

方法1. package.jsonを追加し、"type": "module"を設定する。mainを設定する必要がある。

package.json
{
  "type": "module",
  "main": "./main.js"
}

方法2.--input-type=moduleをつけて実行する

node main.js --input-type=module

方法3. .mjsに拡張子を変える

main.mjs
const functions = require('./libs/functions.mjs')
functions.hoge()

参考元:https://nodejs.org/api/esm.html#enabling

互換性について

CommonJSのモジュールからESMを呼ぶ

CommonJS/main.js
import { hello } from './libs/functions.js'
hello()
const functions = require('ESModules/libs/functions.js')
functions.hello()
❯ node main.js
1234
/Users/yodaka/Desktop/jsStudy/commonJS/main.js:3
const functions2 = require('../ESModules/libs/functions.js')
                   ^

Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/yodaka/Desktop/jsStudy/ESModules/libs/functions.js from /Users/yodaka/Desktop/jsStudy/commonJS/main.js not supported.
Instead change the require of functions.js in /Users/yodaka/Desktop/jsStudy/commonJS/main.js to a dynamic import() which is available in all CommonJS modules.

だめ。

ESMのモジュールからCommonJSを呼ぶ

ESModules/main.js
import { hello } from './libs/functions.js'
hello()
import { hoge } from '../commonJS/libs/functions.js'
hoge()
❯ node main.js
hello
1234

いける!

と言うことでESModuleからCommonJSのモジュールをimportできる

今後について

前述した通り、ES Modulesが現在フロントエンド開発において主流のモジュール方式である。
が、まだCommonJSを捨てきれない理由がある。

ES Modulesに全振りする上での問題点

ES Modules方式のみを採用したライブラリを使いたい場合

ここに書いてある対応をしないとそのライブラリは使えない。
https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm

具体的には

  1. プロジェクト自体をES Modulesにする(もっともおすすめ)
  • "type": "module" を package.jsonに追加する
  • package.jsonの"main": "index.js""exports": "./index.js"に置き換える.
  • package.jsonの"engines"の項目をNode.js 12: "node": "^12.20.0 || ^14.13.1 || >=16.0.0"にあげる
  • 'use strict'を全てのJavaScript fileから消す。
  • 全てのrequire()/module.export をimport/exportに書き直す。
  • importは相対パスに書き換え、ファイル名をフルで書くようにする。(拡張子の省略すら許されない): import x from '.'; → import x from './index.js';.
  • TypeScriptを使ってるなら型定義(index.d.ts)を ESM imports/exportsに変える.

引用:https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-move-my-commonjs-project-to-esm

  1. 動的インポートを採用する
import('../ESModules/libs/functions.js').then(functions2 => {
  functions2.hello()
})

以下も書けるが、awaitはasyncの中でしか使えないので普通にトップレベルのスコープで呼び出すとエラーになる。非同期で呼び出す必要がある。

const module = await import('../ESModules/libs/functions.js')
functions2.hello()
❯ node main.js
/Users/yodaka/Desktop/jsStudy/commonJS/main.js:3
const functions2 = await import('../ESModules/libs/functions.js')
                   ^^^^^

SyntaxError: await is only valid in async functions and the top level bodies of modules
    at Object.compileFunction (node:vm:352:18)

3. そのライブラリをCommonJS方式でモジュール化されているバージョンまで下げて使う。

普通に1が辛い

正直webpackを使ってるならimport,export方式で書いてあるプロジェクトがほとんどだろうから、requireを書き換える作業はほぼないだろう。
しかし問題はここである。

importは相対パスに書き換え、ファイル名をフルで書くようにする。(拡張子の省略すら許されない): import x from '.'; → import x from './index.js';.

拡張子を省略しているプロジェクトは多いはず。全てに拡張子を書くのは正直現実的ではない。
さらにTypeScriptを採用しているプロジェクトはこれだけでは済まず追加で以下が必要になる。

  • tsconfig.jsonに"module": "ES2020"を追加
  • .tsファイルのimport時にも.jsの相対パス指定で呼び出す

.tsを.jsとしてimportしないといけない。
個人的には気持ち悪いが、JavaScriptのセマンティクスをTypeScriptが変更しないようにというポリシーの結果と聞いて納得した。(以下の記事を参照)
https://zenn.dev/teppeis/articles/2021-10-typescript-45-esm#識別子周りの議論

なんでES Modulesにしないといけないの?

引用元
https://blog.sindresorhus.com/get-ready-for-esm-aa53530b3f77

ES Modulesは言語レベルの文法や、ブラウザサポート、デフォルトでstrictモード、非同期import、トップレベルでのawaitの使用、静的解析の向上、ツリーシェイキングなど様々な利点があります。

拙訳ですが、、
language-level syntaxがよくわからないまま言語レベルの文法と訳しました。
トップレベルでのawaitやstrictモードが標準だったり、静的解析が向上するのは嬉しい。

まとめ

CommonJS

  • 古株
  • Node.jsがデフォルトで採用
  • ES Modulesで書かれたモジュールは呼べない

ES Modules

  • 新しい
  • ウェブブラウザがデフォルトで採用
  • Node.jsも最近のバージョンで対応済み
  • CommonJSで書かれたモジュールが呼べる
  • 全てをES Modulesにしていくために今まさに色々進められている

TypeScriptの対応とともにどのライブラリも徐々にPureなES Modulesになって行くのではないかという印象がある。
いい機会だったのでまとめられてよかった。
最新情報や、間違っているところあればコメントいただけると幸いです。

Discussion