CommonJSとES Modulesについてまとめる
モチベーション
普段フロントエンドを領分にしているのになかなかこのあたりの基礎が足りていないと感じることが多いので、なんとかしたい。
ES Modules方式でしか対応されていないライブラリを使おうとしてコケたので色々調べたのも含め、まとめていく。
ちなみにその辺りについてはこの神記事見ると良い。
個人的に気になっているモジュールシステムについて掘り下げていく。
CommonJS
CommonJSとは、サーバーサイドなどのウェブブラウザ環境外におけるJavaScriptの各種仕様を定めることを目標としたプロジェクトである。
from Wikipedia
例えばNode.jsで使われている。
Node.jsはデフォルトで全てのモジュールをCommonJSで扱うが、Node.jsは最近のバージョンでES Modulesに対応するなどしていて、潮流はES Modulesに流れつつある。
後述するが、それでもまだ捨てきれない理由がある。
モジュール
Node.jsはデフォルトで全てのモジュールをCommonJSで扱う。
他モジュールを呼び出す際はrequireで呼び出す。
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を設定する必要がある。
{
"type": "module",
"main": "./main.js"
}
方法2.--input-type=moduleをつけて実行する
node main.js --input-type=module
方法3. .mjsに拡張子を変える
const functions = require('./libs/functions.mjs')
functions.hoge()
参考元:https://nodejs.org/api/esm.html#enabling
互換性について
CommonJSのモジュールからESMを呼ぶ
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を呼ぶ
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方式のみを採用したライブラリを使いたい場合
ここに書いてある対応をしないとそのライブラリは使えない。
具体的には
- プロジェクト自体を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に変える.
- 動的インポートを採用する
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が変更しないようにというポリシーの結果と聞いて納得した。(以下の記事を参照)
なんでES Modulesにしないといけないの?
引用元
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
めちゃめちゃ参考になりました。ありがとうございます。