楽しいだけで十分です

JavaScriptにおけるモジュール機構や、モジュールバンドラーについて

人に説明する機会があるのですが、「説明できるほど理解できてないかも...」となったので、JavaScript(以下「JS」という)のモジュール関連の知識を整理します。

想定読者

  • 何かしらのプログラミング言語経験者 (JS以外の言語の人でもわかるように、特有の言語には注釈をできるだけつけます)

モジュール機構

モジュール機構の登場以前

EcmaScript(JSの言語仕様。後に続く数字はバージョン。以下「ES」という)5以前のJSにはモジュール機構が存在しませんでした。そのため、名前空間の汚染が起こりやすい(e.g. 別ファイル間で同じ変数名を使った場合)という問題がありました。この問題の対応方法として使われていたのが、関数を利用してカプセル化を行う方法でした。JSは関数ごとにスコープを作ることができ、それを関数スコープと言います。関数スコープ内でvar, let, constのいずれかで変数宣言すると、関数内のみで利用可能な(関数外からはアクセスできない)変数(いわゆる、プライベート変数)を定義できます。この特徴を使って、グローバルの名前空間を汚染を最小限にします。

この関数を用いた方法はモジュールパターンと呼ばれており、モジュールパターンの中でも実装方法によって種類がさらに分かれていますが、ここではRevealing Module Patternを紹介します。

var module = (function () {
  var name = "yinm";

  var hello = function () {
    console.log("hello " + name);
  };

  var goodbye = function () {
    console.log("goodbye " + name);
  };

  return {
    hello: hello,
    goodbye: goodbye,
  };
})();

module.hello(); // => 'hello yinm'
module.goodbye(); // => 'hello yinm'
console.log(module.name); // => undefined (プライベート変数のため、関数内の値にアクセスできない)

ただし、この方法でも多少はグローバルの名前空間の汚染が起きています。上記の例だと、moduleという変数が該当します(ブラウザ環境なら、window.module)。この問題も後述するモジュール機構だと解決します。

参考

CommonJS (CJS)

CommonJSとは、JSを汎用言語(ブラウザ以外でもアプリケーション開発できるよう)にするための仕様(文脈によっては、それを策定するプロジェクト)です。先述の通り、ES5以前のJSにはモジュール機構がないなど、汎用言語にするには物足りない部分がありました。「言語仕様にないなら新しく仕様を作ればいいじゃない」ということで生まれたのがCommonJSです。

CommonJSの中のモジュールの仕様に準拠している実行環境としてNode.jsがあります。なお、準拠と言いつつ、Node.js独自の拡張(e.g. module.exportsでもexportできる)もしているので、CommonJSと一致している訳ではないです。

利用方法

参考

余談

ブラウザ向けのモジュール機構として、AMD(Asynchronous Module Definition)というAPI仕様をサポートしたRequireJSもあります。しかし、今日では利用する機会があまりないと思われるので、名前だけの紹介にしておきます。

EcmaScript Modules (ESM)

ここまでJSの言語仕様としてモジュール機構が存在しないと書いてきました。しかし、ES2015でついに言語仕様としてモジュール機構をサポートすることになります。それが、EcmaScript Modules(以下、「ESM」という)です。

利用方法

  • モジュールの読み込み: import
  • モジュール外へ読み込み可能にする: export

ESMにはいくつか特徴があります(ここでは一部を紹介)。

  • 構文(import文, export文)としてサポートしているので、コードの実行前に依存関係を解決できる (CommonJSは仕組み上、実行しないと依存関係を解決できません。)
  • モジュールレベルのスコープがある (モジュール内の変数や関数は、外からアクセスできない)
  • 常に'use strict'になる
  • モジュールコードはimport時の初回のみ評価する
  • トップレベルの “this”undefined

さて、言語仕様はできたのですが、ESMを利用するために必要なモジュールの読み込み方法や依存関係の解決については、言語仕様としては策定されていません。これは、読み込み方法などは実行環境に依存し、言語仕様としては預かり知らない部分であるためです。そのため、読み込み方法などは実行環境(e.g. ブラウザ, Node.js)ごとに仕様が作られることになります。

ブラウザにおいては、<script>タグのtype属性によってモジュールか従来のスクリプトかを判定します。モジュールと判定してほしい場合は、type="module"と書きます。ブラウザの対応状況ですが、 https://caniuse.com/#feat=es6-module にあるようにモダンブラウザはほぼ対応しています。ただし、現状では、パフォーマンスの観点(依存関係の解決の度にリクエストが必要になるネットワーク通信のボトルネックを避けたい)などからモジュールバンドラー(後述)を使うことが一般的かと思います。

Node.jsにおいては、ブラウザにはない2つの問題があり、仕様策定が難航しています。2つの問題とは、以下のものです。

  • 読み込むファイルがCommonJSかESMかわからない (ブラウザ側の対応であるtypeように、読み込み前に判定する術がない)
  • CommonJSとの互換を維持する必要がある

現状で考えられている対応方法は、ECMAScript Modules | Node.js v13.12.0 Documentationにある3つです。

  • 拡張子(ESMの場合は、.mjs)で判定する
  • 一番近くの親のpackage.jsonのtypeフィールド(ESMの場合は、module)で判定する
  • nodeコマンドやevalなどの文字列に--input-typeフラグ(ESMの場合は、module)をつけて判定する(上の2つと比べると、利用箇所が限定的な機能でしょうか?)

参考

モジュールバンドラー

モジュールバンドラーとは、モジュールの依存関係を解決して、1つ以上のファイルに結合する(bundle)ライブラリです。

モジュールバンドラーの理解を深めるために誕生背景を見るべく、時代をNode.jsの登場 ~ ESMの登場前の間に戻します。この頃にはNode.jsを使って実装したモジュールが多く作られます。作成したNode.jsのモジュールを、npm(文脈によって意味が異なりますが、この場合はモジュールのレジストリ)に公開することで、他のNode.jsユーザーもモジュールを利用になるエコシステムが発達していきます。モジュールの中には、Node.jsの実行環境に依存しないもの、つまりブラウザでもCommonJS形式のモジュールを読み込めさえすれば実行可能なものも多くありました。Webフロントエンドの開発でもこれらのモジュールが使えると便利そうだなと人々が思っていたときに、「なら読み込めるようにすればいいじゃない」ということで生まれたのがBrowserifyなどのモジュールバンドラーです。

結合できるモジュールの形式はモジュールバンドラーによって異なります。例えば、BrowserifyはCommonJS形式のモジュールのみですが、webpackはCommonJSだけでなくESMなども結合できます(これはデフォルトの機能だけで比較した場合の話です。プラグインなどの拡張機能を使えば、結合できるモジュール形式を増やすこともできるはずです)。

該当するライブラリの例

  • webpack
  • rollup.js
  • Parcel
  • Browserify

どのモジュールバンドラーがよく使われているかについては、あくまで自分の観測の範囲ですが、以下の感じでしょうか。

  • Webフロントエンドのアプリケーションの開発なら、webpack
  • ライブラリ開発なら、rollup.js

まとめ

JSにおけるモジュール関連の知識として、モジュール機構登場以前の方法・CommonJS・ESMと、モジュールバンドラーの役割について整理しました。

yinm

WRITTEN BY yinm

I'm a Frontend Engineer.