アプリケーションが大きくなるにつれ、それを複数のファイルに分割したくなります。いわゆる ‘モジュール’ です。 通常、モジュールはクラスや便利な関数のライブラリを含みます。

長い間、JavaScript には言語レベルのモジュール構文は存在しませんでした。当初はスクリプトが小さくて単純だったため、それは問題ではありませんでした。そのため、モジュールの仕組みは必要ありませんでした。

しかし、スクリプト徐々に複雑になってきました。そのため、コミュニティはコードをモジュールにまとめるための様々な方法を発明しました。

例えば:

  • AMD – 最も古いモジュールシステムの1つで、最初はライブラリrequire.jsで実装されました。
  • CommonJS – Node.JS サーバ用に作られたモジュールシステムです。
  • UMD – もう1つのモジュールシステムで、ユニバーサルなものとして提案されています。AMD と CommonJS と互換性があります。

今や、これらはゆっくりと歴史の一部になっていますが、依然として古いスクリプトの中で利用されています。言語レベルのモジュールシステムの標準は 2015 年に登場し、それ以来徐々に進化し、今ではすべての主要なブラウザとNode.JS でサポートされています。

モジュールとは?

モジュールは単なる1つのファイルです。

ディレクティブ exportimport を利用することで、モジュール間で機能を相互にやりとりすることができます。:

  • export キーワードは、ファイルの外部からアクセス可能であるべき変数や関数にラベル付けをします。
  • import は他のモジュールから機能をインポートできるようにします。

例えば、関数をエクスポートしているファイル sayHi.js があります:

// 📁 sayHi.js
export function sayHi(user) {
  alert(`Hello, ${user}!`);
}

…そして、別のファイルでそれをインポートして使います。:

// 📁 main.js
import {sayHi} from './sayHi.js';

alert(sayHi); // function...
sayHi('John'); // Hello, John!

このチュートリアルでは言語自身に焦点を当てていますが、デモ環境としてブラウザを利用します。なので、ブラウザでモジュールを動作させる方法を見ておきましょう。

モジュールを使うには、次のように <script type="module"> 属性を設定します。:

結果
say.js
index.html
export function sayHi(user) {
  return `Hello, ${user}!`;
}
<!doctype html>
<script type="module">
  import {sayHi} from './say.js';

  document.body.innerHTML = sayHi('John');
</script>

ブラウザは自動的にインポート対象を取得/評価し、スクリプトを実行します。

コアなモジュールの特徴

“通常の” スクリプトと比較してときのモジュールの違いは何でしょう?

ブラウザとサーバサイト JavaScript の両方に有効なコアな特徴があります。

常に “use strict”

モジュールは常に use strict です。E.g. 未宣言変数への代入はエラーになります。

<script type="module">
  a = 5; // error
</script>

ここではブラウザで確認していますが、同じことはどのモジュールにも当てはまります。

モジュールレベルのスコープ

各モジュールには独自の最上位のスコープがあります。つまり、モジュール内の最上位の変数や関数は他のスクリプトからは見えません。

下の例では、2つのスクリプトがインポートされており、hello.jsuser.js で宣言されている変数 user を使おうとして、失敗します:

結果
hello.js
user.js
index.html
alert(user); // no such variable (each module has independent variables)
let user = "John";
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

モジュールは、外部からアクセス可能にしたいものは export をし、必要なものは import することを期待しています。

したがって、index.html の代わりに、直接 hello.jsuser.js をインポートする必要があります。

これは正しい例です:

結果
hello.js
user.js
index.html
import {user} from './user.js';

document.body.innerHTML = user; // John
export let user = "John";
<!doctype html>
<script type="module" src="hello.js"></script>

ブラウザでは、各 <script type="module"> に対しても独立した最上位スコープが存在します。:

<script type="module">
  // 変数はこのモジュールスクリプトの中でのみ見えます
  let user = "John";
</script>

<script type="module">
  alert(user); // Error: user is not defined
</script>

もしも、本当に “グローバルな” ブラウザ内変数を作る必要がある場合は、それを明示的に window に割り当て、window.user としてアクセスします。しかし、これは例外であり正当な理由がある場合のみです。

モジュールコードはインポート時の初回にのみ評価されます

もし同じモジュールが複数の他の場所でインポートされる場合、そのコードは初回のみ実行されます。その後エクスポートしたものはすべてのインポートしているモジュールで利用されます。

これは重要な結果をもたらします。例を見てみましょう。

まず、メッセージを表示すると言ったような、副作用をもたらすモジュールコードを実行する場合、複数回インポートしてもトリガされるのは1度だけです(初回)。:

// 📁 alert.js
alert("Module is evaluated!");
// 別のファイルから同じモジュールをインポート

// 📁 1.js
import `./alert.js`; // Module is evaluated!

// 📁 2.js
import `./alert.js`; // (nothing)

実際、最上位のモジュールコードは主に初期化に使われます。データ構造を作成し、それらを事前に設定します。そして、何かを再利用可能にしたいとき、それらをエクスポートします。

これはより高度な例です。

モジュールがオブジェクトをエクスポートするとしましょう:

// 📁 admin.js
export let admin = {
  name: "John"
};

このモジュールが複数のファイルからインポートされた場合、モジュールは初回にだけ評価され、admin オブジェクトが生成され、その後このモジュールをインポートするすべてのモジュールに渡されます。

すべてのインポータは正確に1つの admin オブジェクトを取得することになります。:

// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";

// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete

// 1.js と 2.js 同じオブジェクトをインポートしました
// 1.js で行われた変更は 2.js でも見えます

繰り返しましょう – モジュールは一度だけ実行されます。エクスポートが生成され、それがインポータ間で共有されます。そのため、なにかが admin オブジェクトを変更した場合、他のモジュールにもその変更が見えます。

このような振る舞いは、コンフィグレーションが必要なモジュールにとっては便利です。最初のインポートで必要なプロパティを設定することができ、以降のインポートではすでに準備ができている状態です。

例えば、admin.js モジュールは特定の機能を提供するかもしれませんが、外部から admin オブジェクトにクレデンシャル情報が来ることを期待します。:

// 📁 admin.js
export let admin = { };

export function sayHi() {
  alert(`Ready to serve, ${admin.name}!`);
}

ここで、init.js は我々のアプリケーションの最初のスクリプトで、admin.name をセットします。その後、admin.js 自身の内側から行われる呼び出しを含め、誰もがそれを見ることができます。:

// 📁 init.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 other.js
import {admin, sayHi} from './admin.js';

alert(admin.name); // Pete

sayHi(); // Ready to serve, Pete!

import.meta

オブジェクト import.meta は現在のモジュールに関する情報を含んでいます。

この内容は環境に依存します。ブラウザでは、スクリプトの url、HTML 内であれば現在のウェブページの url を含んでいます。:

<script type="module">
  alert(import.meta.url); // script url (インラインスクリプトに対する HTML ページの url)
</script>

最上位の “this” は undefined

これは小さな特徴ですが、完全性のために言及しておきます。

モジュールでは、最上位の this は非モジュールスクリプトにおけるグローバルオブジェクトとは対照的に、undefined です。

<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>

ブラウザ固有の特徴

通常のスクリプトと比べて、type="module" を持つスクリプトには、ブラウザ固有の違いもいくつかあります。

もし初めて読んでいる場合、またはブラウザで JavaScript を使用していない場合はスキップしても構いません。

モジュールスクリプトは遅延されます

モジュールスクリプトは外部スクリプトとインラインスクリプト両方で、常に 遅延され、defer 属性(チャプター ページのライフサイクル: DOMContentLoaded, load, beforeunload, unload で説明しています)と同じ効果を持ちます。

つまり:

  • 外部モジュールスクリプト <script type="module" src="..."> は HTML 処理をブロックしません。
  • モジュールスクリプトは HTML ドキュメントが完全に準備できるまで待ちます。
  • 相対的な順序は維持されます: ドキュメントの最初にあるスクリプトが最初に実行されます。

副作用として、モジュールスクリプトは常にその下の HTML 要素が見えます。

例:

<script type="module">
  alert(typeof button); // object: スクリプトは下のボタンが `見え` ます
  // モジュールは遅延されるので、スクリプトはページ全体がロードされた後に実行します
</script>

<script>
  alert(typeof button); // Error: button is undefined, スクリプトは下の要素は見えません
  // 通常のスクリプトは、ページの残りが処理される前に即時実行します。
</script>

<button id="button">Button</button>

注意: 実際には1つ目のスクリプトの前に2つ目のスクリプトが動作します! なので、最初に undefined が表示され、その後 object が表示されます。

これは、モジュールが遅延されているためです。通常のスクリプトはすぐに実行するので、最初に出力されます。

モジュールを使うときは、JavaScript アプリケーションが準備できる前に HTML ドキュメントが表示できることに注意してください。一部の機能はまだ機能しない可能性があります。透明なオーバーレイ、または "ローディング"を配置する、もしくはそれ以外の方法で訪問者が混乱しないようにする必要があります。

Async はインラインスクリプトで動作します

非同期(Async)属性 <script async type="module"> はインライン、外部スクリプトの両方で使用できます。非同期スクリプトは、インポートされたモジュールが処理されるとすぐに実行されます。他のスクリプトや HTML ドキュメントとは独立しています。

例えば、下のスクリプトは async があるので、誰かを待つことはありません。

それは、たとえ HTMl ドキュメントがまだ完了していない場合や、他のスクリプトがまだ保留の場合でも、インポート( ./analytics.js の取得)を行い、準備ができたときに実行します。

これはカウンタや広告、ドキュメントレベルのイベントリスナなど、何にも依存しない機能に適しています。

<!-- すべての依存対象が取得(analytics.js)され、スクリプトが実行されます -->
<!-- ドキュメントや他の <script> タグは待ちません -->
<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>

外部スクリプト

外部モジュールスクリプトには、2つの大きな違いがあります。:

  1. 同じ src の外部スクリプトは一度だけ実行されます:

    <!-- スクリプト my.js は一度だけ取得され実行されます -->
    <script type="module" src="my.js"></script>
    <script type="module" src="my.js"></script>
  2. 別のドメインから取得された外部スクリプトはCORS ヘッダを必要とします。言い換えると、モジュールスクリプトが別のドメインから取得された場合、リモートサーバはその取得が許可されていることを示すために、ヘッダ Access-Control-Allow-Origin: * (* の代わりに取得するドメインを指定する場合もあります)を提供しなければなりません。

    <!-- another-site.com は Access-Control-Allow-Origin を提供しなければなりません -->
    <!-- そうでない場合、スクリプトは実行されません -->
    <script type="module" src="http://another-site.com/their.js"></script>

    これにより、デフォルトでセキュリティが向上します。

ベア(剥き出しの) モジュールは許可されていません

ブラウザでは、スクリプトの中(HTML ではない)で、import は相対URLか絶対URL、どちらかの取得が必須です。 パスのない、いわゆる “剥き出しの” モジュールは許可されていません。

例えば、この import は無効です:

import {sayHi} from 'sayHi'; // Error, "bare" module
// './sayHi.js' またはモジュールの場所でなければなりません

Node.js やバンドルツールのような特定の環境では、モジュールを見つけるための独自の方法や、それらを調整するためのフックがあるため、剥き出しのモジュールを使用することができます。しかしブラウザではまだベアモジュールはサポートされていません。

互換性, “nomodule”

古いブラウザは type="module" を理解しません。未知のタイプのスクリプトは単に無視されます。それらには、nomodule 属性を使って、フォールバックを提供することが可能です。:

<script type="module">
  alert("Runs in modern browsers");
</script>

<script nomodule>
  alert("現在のブラウザは type=module と nomodule どちらも知っているので、これはスキップされます")
  alert("古いブラウザは未知の type=module を持つスクリプトは無視しますが、これは実行します");
</script>

バンドルツールを使用する場合、モジュールも一緒にバンドルされると、それらの import/export 文は特別なバンドル呼び出しに置き換えられます。したがって、結果として生じるビルドは type=module を必要としません。なので、それを通常のスクリプトに置くことができます。:

<!-- Webpack のようなツールから bundle.js を得た想定 -->
<script src="bundle.js"></script>

ビルドツール

現実には、ブラウザモジュールが “生” の形式で使用されることはほとんどありません。通常、それらを Webpack などの特別なツールを使って一緒にまとめて、プロダクションサーバにデプロイします。

バンドラーを使用する利点の1つは、それらはモジュールをどのように解決するかについてより多くの制御を与えることができ、CSS/HTML モジュールのようにベアモジュールやその他のことを可能にします。

ビルドツールは次のことを行います。:

  1. HTML の <script type="module"> 置くことを意図した “メイン” モジュールを取ります。
  2. 依存関係を分析します: インポート、インポートのインポート…など
  3. ネイティブの import 呼び出しをバンドラ関数で置き換え、すべてのモジュールを1つのファイルにビルドします(もしくは複数のファイルにします。調整可能)。
  4. その過程で、他の変換や最適化を適用することができます。
    • 到達不能のコードの削除
    • 未使用のエクスポートの削除(“tree-shaking”)
    • consoledebugger のような開発固有の文の削除
    • 最先端の JavaScript 構文は、Babel] を使用して同様の機能を持つ古い構文に変換されます
    • 結果のファイルの minify (スペースの削除、変数を短い名前に置換するなど)

とはいえ、ネイティブモジュールも使用可能です。なので、ここでは Webpack は使用しません。もちろん後で設定することは可能です。

サマリ

まとめると、コアの概念は次の通りです:

  1. モジュールはファイルです。import/export を機能させるには、ブラウザは <script type="module"> を必要とし、それはいくつかの違いを意味します。:
    • デフォルトでは遅延
    • 非同期はインラインスクリプトで動作する
    • 外部スクリプトは CORS ヘッダを必要とする
    • 重複した外部スクリプトは無視される
  2. モジュールは独自のローカルの最上位スコープを持ち、import/export 経由で、モジュール間で機能をやり取りします。
  3. モジュールは常に use strict です。
  4. モジュールコードは一度だけ実行されます。エクスポートは一度生成され、インポータ間で共有されます。

したがって、通常、モジュールを使用するとき、各モジュールは機能を実装し、それらをエクスポートします。そして、import を使って、必要な場所に直接インポートします。ブラウザは自動的にスクリプトを読み込み、評価します。

プロダクション環境では多くの場合、パフォーマンスや他の理由で、モジュールを1つにまとめるために Webpack などのバンドラを使用します。

次のチャプターでは、より多くのモジュールの例と、どのようにエクスポート/インポートされるかを見ていきます。

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。