メインコンテンツまでスキップ

🚧モジュール

caution

Experimental! このページは執筆途中の草稿です。構成が大きく変わることがありえますので、リンクなどをする場合はリンク切れが起こる可能性がある点をご留意ください。このページの内容は、import、export、requireをもとに再構成・加筆しています。

モジュールの基礎

モジュールの目的

プログラムは大きさがさまざまです。行数で見ると、数行のものから数万行のものまであります。

小さいプログラムなら、1つのファイルでも十分ですが、大きなプログラムは1つのファイルだけで作るのは大変です。

想像してみてください。何千行もあるプログラムが1つのファイルに詰め込まれている状態は、読みにくく、修正もしにくくなります。

  1. 保守性の問題 ─ 保守性が低いことです。大量のコードが1つのファイルに詰め込まれているため、変更がしにくいです。コードの見通しが悪いため、1行の変更が他の数千行にどのような影響を与えるのか、予想もつかないことがあります。これは、変更に対して臆病になってしまう原因にもなります。
  2. 変数名の衝突 ─ コードが長いと、変数名が衝突する危険性が高まります。これにより、関係のない変数を上書きしてしまう恐れがあります。これを避けるために変数名を長くすることもありますが、それは可読性を損ねる原因になります。
  3. 再利用の問題 ─ たとえば、数千行のコードの中から特定の部分だけを別のプロジェクトで使用したい場合、それらが1つの大きな塊にまとめられているため、必要な部分だけを抜き出せません。無理に読み込むと、不要なコードも一緒に読み込んでしまい、それがどう悪さをするかは予想がつきません。

このような問題を解決するのが、モジュール(module)と呼ばれる仕組みです。モジュールは1つのファイルを複数のファイルに分割し、関連付けて、ひとつのプログラムとして動かすことができます。

大規模なプログラムを作る場合、それぞれの機能ごとにモジュールを分けることで、各モジュールを読みやすく、保守性も高く、再利用もしやすくなります。

パッケージとモジュールの違い

モジュールと似た用語に、パッケージ(package)という言葉があります。プログラミング言語によって、モジュールとパッケージの定義は異なります。JavaScriptでは、これらはどのように捉えられているでしょうか。

モジュールは、基本的にひとつひとつのJavaScript/TypeScriptファイルを指します。詳細は「スクリプトとモジュール」のセクションで説明しますが、JavaScript/TypeScriptファイルのうち、exportまたはimportを1つ以上含んだものがモジュールに当たります。

パッケージは、package.jsonとJavaScriptファイル群を持つディレクトリを指します。package.jsonは、パッケージの名前、バージョン、ライセンスなどのメタデータを記載したファイルです。

モジュールとパッケージには、利用用途の違いがあります。一般的なアプリケーション開発では、複数のJavaScript/TypeScriptファイルに分けて開発します。この際に作られるファイルひとつひとつがモジュールになります。アプリケーションの保守性の担保、コードの再利用性の確保、変数名衝突の回避のために用いられるのがモジュールです。

一方、パッケージの典型的な目的は配布です。ライブラリ製作者が、プログラムを他者に配布する際に用いられるのがパッケージです。そして、アプリケーション開発者は、自己のアプリケーションにパッケージを組み込む形で、パッケージは利用されていきます。

モジュールとエコシステム

バンドラー

JavaScriptでは、複数のJavaScriptファイルを1つのファイルに繋ぎ合わせることをバンドル(bundle)と言います。バンドルを、自動的に行なう開発ツールを、バンドラー(bundler)と呼びます。バンドラーは「モジュールバンドラー」と呼ばれることもあります。

JavaScriptには、さまざまなバンドラーがあります。たとえば、有名なものとしては次のようなバンドラーがあります。

  • webpack
  • rollup
  • parcel
  • esbuild
  • vite

これらのバンドラーは、JavaScriptだけではなく、TypeScriptやCSS、画像などさまざまな種類のファイルをバンドルできます。

バンドラーが必要な理由

JavaScriptのバンドラーを使わないと、Webアプリケーションを実行するために、複数のJavaScriptファイルを個別に読み込む必要があります。これは、いくつかの問題を生みます。

第一に、ウェブブラウザーがJavaScriptのコードを読み込む際に、多くの時間がかかるようになります。

第二に、JavaScriptファイル同士の依存関係が整理されないことに起因して、コードが壊れ、バグが起こることがあります。

第三に、JavaScriptコードが最適化された書き方になっていない場合、アプリケーションの実行パフォーマンスが悪くなることがあります。

このような問題を解決するがバンドラーの役割です。

サーバーサイドJSではバンドラーの利点は少ない

JavaScriptはウェブブラウザーで実行するために作られた言語ですが、サーバーサイドでも使えます。サーバーサイドのJavaScript実行環境のひとつにNode.jsがあります。Node.jsは、古くからモジュールシステムを内部で実装しているため、JavaScriptにESモジュールのようなモジュールシステムがまだ無い時代から、モジュールが使えていました。そのため、サーバーサイドJavaScriptの環境では、バンドラーの必要性が生まれませんでした。

昨今のJavaScriptはモジュールシステムを持っているため、バンドラーを使わなくともモジュールを実現できます。これはフロントエンドでも同様です。しかしながら、フロントエンドでは数百、数千のモジュールをバラバラにダウンロードして実行するのは時間がかかるため、ひとつのJavaScriptファイルにまとめるバンドラーの役割というのは、依然として重要です。

一方で、サーバーサイドでは、モジュールが数百、数千とあったとしても、モジュールをロードするのはサーバー起動時だけです。そのため、バンドラーの利点はほぼありません。

モジュールシステム

CommonJSとESモジュール

CommonJSとESモジュールが混在している理由

これをお読みの皆さんの中には、JavaScriptやTypeScript以外のプログラミング言語を経験したことがある人もいるかと思います。他の言語で、複数のモジュールシステムが共存している言語を使ったことはありますでしょうか。

JavaScriptには、系統が異なるモジュールシステムが、少なくとも2つ存在しています。ESモジュールとCommonJSです。こうした状況は、プログラミング言語としては、珍しいことです。JavaScriptのモジュールまわりを理解するのを難しくしている要因でもあります。

では、どうしてJavaScriptは2系統もモジュールシステムを持つようになったのでしょうか? ここでは、JavaScriptの現状に至る流れを歴史からひも解いていきます。

ひとつめのモジュールシステム

JavaScriptのモジュールシステムは、ブラウザよりも先んじて、サーバーサイドJavaScript、とりわけNode.jsの文脈で発展してきた経緯があります。

JavaScriptで広く普及しているモジュールシステムのひとつがCommonJSです。CommonJSの歴史をさかのぼると、2009年のServerJS発足に至ります。ServerJSはJavaScriptをサーバーサイドで使えるようにすることを目指し、サーバーサイドJavaScriptの共通APIを策定する標準化プロジェクトでした。のちに、CommonJSに改名されます。

サーバーサイドにJavaScriptを持ってくると一言で言っても、ブラウザのJavaScriptをそのまま持ち込んでもうまくいきません。たとえば、ブラウザには<script>タグがあるので、ひとつのページに複数の<script>タグを書くことで、複数のJavaScriptが実行できます。一方、サーバーサイドにはページという概念がありません。

また、当時のJavaScriptにはESモジュールのようなモジュールシステムもありませんでした。そのため、JavaScriptファイルを複数ロードできる仕組みを考えるところから始めなければなりませんでした。

そこで考え出された仕様がCommonJSのモジュールです。おなじみのrequire()module.exportsです。CommonJSは、モジュールシステムが存在しない当時のJavaScriptの文法や機能の枠を超えずに、関数や変数で工夫することで、モジュール的なものを成立させるものでした。

Node.jsはCommonJSと同時期にリリースされましたが、Node.jsに採用されたモジュールシステムがこのCommonJSでした。このおかげで、Node.jsにおいてはサーバーサイドJavaScriptでもファイルの分割と、複数ファイルのロードができるようになっていました。このモジュールシステムは、Node.jsユーザーに受け入れられはじめ、ライブラリを公開できるnpmなど、モジュールまわりのエコシステムも発展していきました。

ちなみに、CommonJSやNode.jsがスタートした2009年の前年には、ECMAScript 4草案破棄というショッキングな出来ごとがありました。ES4には、モジュールシステムをJavaScriptに追加する仕様も盛り込まれていました。もし、ES4が実現していたら、CommonJSは必要無かったかも知れません。現実はESの仕様を決めるブラウザベンダー間での意見と対立があり、JavaScriptを改善する動きは仲たがいで終わってしまいました。

Node.jsの登場時期がそんなバッドタイミングだったこともあり、JavaScript自体がモジュールシステムを改善するのはなかなか期待できない状況でした。そのため、Node.jsは既存のJavaScriptでできる範囲内の解決策として、モジュールシステムにCommonJSを採用したという見方もできます。

CommonJSはサーバーサイドで生まれ、発展してきました。CommonJSの土台に乗ったライブラリも数多く作られました。こうしたライブラリは、クライアントサイドでも需要がありました。そのため、webpackを筆頭にモジュールバンドラーはCommonJSをサポートしてきました。CommonJSの生い立ちはサーバーサイドではあったものの、モジュールバンドラーの対応によって、フロントエンドもCommonJSに頼る状況が醸成されました。

ふたつめのモジュールシステム

CommonJSの誕生から歴史は流れ、2015年になると、ES6という新しいJavaScriptの標準仕様が確定します。これはJavaScriptの10年数ぶりの大型アップデートです。そこには、ES6 Modulesというモジュールシステムを実現するための仕様も盛り込まれていました。皆さんご存知のimport文とexport文です。これは、JavaScript初のJavaScriptネイティブのモジュールシステムです。CommonJSが草の根活動で規格化されたモジュールシステムだとすると、ESモジュールは本家が発表した公式的・標準的なモジュールシステムだとも言えます。

JavaScript界は、サーバーサイドもクライアントサイドも関係なく、ES6に対応する中で、ES6 Modulesも導入する方向になり、2016年頃からESモジュール導入に向けて議論が始まりました。議論の中心は、やはり、在来のモジュールシステムであるCommonJSと新システムのESモジュールの共存についてです。

ESモジュールの仕様が確定する頃には、JavaScriptはCommonJS前提とした環境ができあがっていて、CommonJSに準拠したNPMパッケージも沢山あったため、CommonJSを切り捨てる選択肢はありませんでした。もしも、CommonJSを切り捨ててしまうと、過去の資産をほぼすべて失うことになるわけで、CommonJSとESモジュールの共存はNode.jsにとって重要なテーマだったのです。

たとえば、サーバーサイドJavaScriptのNode.jsひとつとっても、長い議論のすえ、2017年にNode.js v8.5.0にて、ESモジュールが実験的な機能としてリリースされます。その後、2019年にv13.2.0にて、ESモジュールから「実験的な機能」というラベルが外れ、プロダクションで使われることを想定した機能に昇格しました。そして2020年には、CommonJSの名前付きエクスポートがESモジュールの名前付きインポートでロードできるようになり、次第にNode.jsでESモジュールを動かす環境が整ってきています。

ESモジュール環境が整備されてきたとは言っても、CommonJSは10年以上、JavaScriptを支えてきており、もはや切っても切れない関係になっています。そのため、今日現在においては、2つのモジュールシステムがJavaScriptに生きているわけです。

まとめ

  • JavaScriptにはCommonJSとESモジュールの2つのモジュールシステムがある。
  • CommonJSはJSと10年以上に及ぶ長く深いつながりがある。
  • JS界はCommonJSとESモジュールが共存する道を選んだ。

CommonJSとESモジュールの違い

importrequireの違い

JavaScriptでは、モジュールから変数などの値をインポートする際に、importrequireを用います。この2つはよく似ていますが、それぞれ異なるモジュールシステムの書き方です。

JavaScriptのモジュールシステムはいくつかありますが、代表的なものが次の2つです。

  • ESモジュール
  • CommonJS

この2つのモジュールシステムの違いの詳細は、(TODO参照先記事記載) で解説していますので、そちらをご覧ください。

import

importは、JavaScriptのモジュールシステムのひとつであるESモジュールで用いる構文です。importは他のモジュールでエクスポートされた変数や関数をインポートするのに用いられます。たとえば、次のような使い方ができます。

js
import { myVariable, myFunction } from "./myModule";
js
import { myVariable, myFunction } from "./myModule";
require

一方で、requireは、CommonJSというモジュールシステムで用いる関数です。require関数は、他のモジュールから変数や関数をインポートするのに使われます。たとえば、次のような使い方になります。

js
const { myVariable, myFunction } = require("./myModule");
js
const { myVariable, myFunction } = require("./myModule");

exportmodule.exportsの違い

importrequireは、他のモジュールから値をインポートするためのものでした。これと対をなすものとして、exportmodule.exportがあります。これらは、他のモジュールに値をエクスポートするためのものです。exportmodule.exportも、それぞれ異なるモジュールシステムで使われます。

export

JavaScriptのexportは、ESモジュールというモジュールシステムで用いる構文です。exportを使うと、モジュール内で定義した変数や関数などを、エクスポートすることができます。たとえば、次のような使い方ができます。

js
export const myVariable = "foo";
export const myFunction = () => {
/* 関数の処理 */
};
js
export const myVariable = "foo";
export const myFunction = () => {
/* 関数の処理 */
};
module.exports

一方で、module.exportsは、CommonJSというモジュールシステムで用いる変数です。CommonJSでは、モジュール内で定義された変数や関数を、module.exportsに代入することで、エクスポートできます。たとえば、次のような書き方になります。

js
module.exports.myVariable = "foo";
module.exports.myFunction = () => {
/* 関数の処理 */
};
js
module.exports.myVariable = "foo";
module.exports.myFunction = () => {
/* 関数の処理 */
};

モジュール解決

ESモジュールの構文

ESモジュールの仕様

CommonJSのAPI

TypeScriptとモジュール

ESモジュールのベストプラクティス

  • 質問する ─ 読んでも分からなかったこと、TypeScriptで分からないこと、お気軽にGitHubまで🙂
  • 問題を報告する ─ 文章やサンプルコードなどの誤植はお知らせください。