2022年10月14日

モジュール、導入

アプリケーションが大きくなると、複数のファイルに分割して、「モジュール」と呼ばれるようになります。モジュールには、特定の目的に対応するクラスや関数のライブラリが含まれる場合があります。

長い間、JavaScriptには言語レベルのモジュール構文がありませんでした。当初、スクリプトは小さくシンプルで、ニーズがなかったため、これは問題ではありませんでした。

しかし、次第にスクリプトはより複雑になっていったため、コミュニティはコードをモジュールに整理するさまざまな方法、オンデマンドでモジュールをロードするための特殊なライブラリを発明しました。

いくつか挙げると(歴史的理由から)

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

これらすべては歴史の一部になりつつありますが、古いスクリプトではまだ見つけることができます。

言語レベルのモジュールシステムは2015年に標準に登場し、それ以来徐々に進化し、現在ではすべての主要ブラウザとNode.jsでサポートされています。したがって、これからは最新のJavaScriptモジュールについて学習していきます。

モジュールとは?

モジュールは単なるファイルです。1つのスクリプトは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!

import ディレクティブは、現在のファイルに対するパス ./sayHi.js でモジュールを読み込み、エクスポートされた関数 sayHi を対応する変数に代入します。

ブラウザの例を実行してみましょう。

モジュールは特別なキーワードや機能をサポートするため、属性 <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>

ブラウザは、インポートされたモジュール(および必要に応じてそのインポート)を自動的にフェッチして評価してから、スクリプトを実行します。

モジュールは HTTP(s) 経由でのみ機能し、ローカルでは機能しません

file:// プロトコルを介して Web ページをローカルに開こうとすると、import/export ディレクティブが機能しないことがわかります。モジュールをテストするには、static-server などのローカルの Web サーバを使用するか、VS Code などのエディタの「ライブサーバー」機能、Live Server Extension を使用します。

コアモジュールの機能

モジュールと「通常の」スクリプトの違いは何でしょうか?

ブラウザとサーバー側の JavaScript のどちらも有効なコア機能があります。

常に「strict」を使用

モジュールは常に strict モードで動作します。たとえば、宣言されていない変数に代入するとエラーが発生します。

<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>

モジュールは、外部からアクセスできるようにするためにエクスポートするものをエクスポートし、必要なものをインポートする必要があります。

  • user.jsuser 変数をエクスポートする必要があります。
  • hello.jsuser.js モジュールからそれをインポートする必要があります。

言い換えると、モジュールではグローバル変数に依存するのではなく、import/export を使用します。

これが正しいバリエーションです

結果
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>

ブラウザでは、HTML ページについて説明すると、各 <script type="module"> に対しても独立した最上位レベルのスコープが存在します。

同じページに type="module" の 2 つのスクリプトがあります。それらは互いの最上位レベルの変数を認識しません

<script type="module">
  // The variable is only visible in this module script
  let user = "John";
</script>

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

ブラウザでは、変数を window プロパティ(たとえば window.user = "John")に明示的に代入することで、ウィンドウレベルのグローバル変数にすることができます。

そうすると、type="module" の有無にかかわらず、すべてのスクリプトがそれを認識します。

ただし、このようなグローバル変数の作成は推奨されません。極力避けてください。

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

同じモジュールが他の複数のモジュールにインポートされた場合、そのコードは最初のインポート時にのみ実行されます。その後、エクスポートは他のすべてのインポーターに提供されます。

一度だけの評価には重要な結果があるので、認識しておく必要があります。

いくつかの例を見てみましょう。

まず、モジュールコードの実行によってメッセージを表示するなどの副作用が発生する場合、そのモジュールを複数回インポートしても、一度だけ(最初の時)のみトリガーされます

// 📁 alert.js
alert("Module is evaluated!");
// Import the same module from different files

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

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

モジュールはすでに評価されているため、2 回目のインポートは何も表示しません。

ルールがあります。トップレベルモジュールコードは、初期化、モジュール固有の内部データ構造の作成に使用されます。何かを複数回呼び出す必要がある場合は、上でsayHiで行ったように、それを関数としてエクスポートする必要があります。

それでは、より深く例を考えてみましょう。

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

// 📁 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

// Both 1.js and 2.js reference the same admin object
// Changes made in 1.js are visible in 2.js

ご覧のように、1.jsでインポートされたadminnameプロパティを変更すると、2.jsで新しいadmin.nameを確認できます。

これはモジュールが1回だけ実行されるためです。エクスポートが生成され、インポータ間で共有されるため、adminオブジェクトが変更された場合は、他のインポータにも反映されます。

このような動作は実際には非常に便利です。モジュールを構成できるからです。

言い換えると、モジュールは、セットアップが必要な一般的な機能を提供できます。たとえば、認証には資格情報が必要です。その後、外部コードがそれを割り当てることを期待して、構成オブジェクトをエクスポートできます。

古典的なパターンを以下に示します。

  1. モジュールは、構成オブジェクトなどの構成方法をエクスポートします。
  2. 最初のインポート時に、それを初期化し、プロパティに書き込みます。トップレベルのアプリケーションスクリプトがこれを行う場合があります。
  3. それ以降のインポートはモジュールを使用します。

たとえば、admin.jsモジュールは特定の機能(例:認証)を提供することがありますが、資格情報は外部からconfigオブジェクトに入ることを期待しています。

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

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

ここで、admin.jsconfigオブジェクト(最初は空ですが、デフォルトのプロパティもある場合があります)をエクスポートします。

その後、アプリの最初のスクリプトであるinit.jsで、そこからconfigをインポートし、config.userを設定します。

// 📁 init.js
import {config} from './admin.js';
config.user = "Pete";

…これでadmin.jsモジュールが構成されました。

それ以降のインポータは呼び出すことができ、現在のユーザーが正しく表示されます。

// 📁 another.js
import {sayHi} from './admin.js';

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

import.meta

import.metaオブジェクトには、現在のモジュールに関する情報が含まれています。

その内容は、環境によって異なります。ブラウザでは、HTML内の場合、スクリプトのURLまたは現在のウェブページのURLが含まれています。

<script type="module">
  alert(import.meta.url); // script URL
  // for an inline script - the URL of the current HTML-page
</script>

モジュールでは、「this」は未定義です。

これはマイナーな機能ですが、完全にするため、ここで言及しておきます。

モジュールでは、トップレベルのthisは未定義です。

thisがグローバルオブジェクトであるモジュール以外のスクリプトと比較してください。

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

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

ブラウザ固有の機能

ここでは、type="module"を持つスクリプトと通常のスクリプトのブラウザ固有の違いをいくつか示します。

初めて読んだり、ブラウザでJavaScriptを使用していない場合は、このセクションはスキップしてもかまいません。

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

モジュールスクリプトは、外部スクリプトとインラインスクリプトの両方で、常に延期されます。これはdefer属性と同じ効果です(スクリプト:async、deferの章で説明)。

言い換えると

  • 外部モジュールスクリプトをダウンロード<script type="module" src="...">対象は、HTML処理をブロックせず、他のリソースと並行して読み込まれます。
  • モジュールスクリプトは、HTMLドキュメントが完全に準備されるまで(サイズが小さく、HTMLより高速に読み込まれた場合でも)待機してから実行されます。
  • スクリプトの相対的な順序が維持されます。ドキュメント内の最初のスクリプトが最初に実行されます。

副作用として、モジュールスクリプトは、下にあるHTML要素を含む、完全に読み込まれたHTMLページを常に「参照」します。

たとえば

<script type="module">
  alert(typeof button); // object: the script can 'see' the button below
  // as modules are deferred, the script runs after the whole page is loaded
</script>

Compare to regular script below:

<script>
  alert(typeof button); // button is undefined, the script can't see elements below
  // regular scripts run immediately, before the rest of the page is processed
</script>

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

注意

2番目のスクリプトは、実際に1番目のスクリプトより先に動作します。なので、最初はundefinedが表示され、その後objectが表示されます。

これは、モジュールは遅延されるため、ドキュメントが処理されるのを待機するためです。通常のスクリプトは直ちに実行されるため、最初にその出力が表示されます。

モジュールを使用する場合、HTMLページが表示されるときに読み込まれるが、JavaScriptモジュールはその後で実行されるので、ユーザーはJavaScriptアプリケーションの準備が完了する前にページを表示することになる可能性があります。まだ機能しない可能性のある機能もあります。そこで、「読み込みインジケーター」を配置するか、それによって訪問者に混乱が生じないようにする必要があります。

非同期はインラインスクリプトで機能します

非モジュールスクリプトの場合、async属性は外部スクリプトでしか機能しません。非同期のスクリプトは、他のスクリプトやHTMLドキュメントとは無関係に、準備が整い次第、直ちに実行されます。

モジュールスクリプトの場合、インラインスクリプトでも機能します。

例えば、以下のインラインスクリプトにはasyncがあるため、何の待機も行いません。

インポートを実行し(./analytics.jsを読み込みます)準備ができしだい、HTMLドキュメントが終わっていなくても、他のスクリプトが保留中でも実行されます。

<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>

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

外部スクリプト

  1. type="module"のある外部スクリプトは、2つの点で異なります

    <!-- the script my.js is fetched and executed only once -->
    <script type="module" src="my.js"></script>
    <script type="module" src="my.js"></script>
  2. 同じsrcを持つ外部スクリプトは一度だけ実行されます

    <!-- another-site.com must supply Access-Control-Allow-Origin -->
    <!-- otherwise, the script won't execute -->
    <script type="module" src="http://another-site.com/their.js"></script>

    別のオリジン(例えば別のサイト)から取得される外部スクリプトは、章フェッチ:オリジンをまたいだリクエストで説明されているCORSヘッダーが必要です。言い換えると、モジュールスクリプトが別のオリジンから取得された場合、リモートサーバーはフェッチを許可するヘッダーAccess-Control-Allow-Originを提供する必要があります。

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

モジュールに「bare」は許可されていません

ブラウザでは、`import`は相対URLか絶対URLを取得する必要があります。パスを持たないモジュールは「bare」モジュールと呼ばれます。このようなモジュールは`import`では許可されていません。

import {sayHi} from 'sayHi'; // Error, "bare" module
// the module must have a path, e.g. './sayHi.js' or wherever the module is

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

Node.jsやバンドルツールなどの特定の環境では、モジュールを見つけて調整するための独自の方法があるため、パスを持たないbareモジュールが許可されます。しかし、まだブラウザではbareモジュールはサポートされていません。

互換性、「nomodule」

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

<script nomodule>
  alert("Modern browsers know both type=module and nomodule, so skip this")
  alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>

古いブラウザは type="module" を理解しません。不明なタイプのスクリプトは無視されます。それらに対しては、nomodule属性を使用してフォールバックを提供できます

ビルドツール

実際、ブラウザモジュールは「生の」形式ではほとんど使用されません。通常は、Webpackなどの特別なツールでそれらをまとめ、本番サーバーにデプロイします。

バンドラを使用する利点の1つは、モジュールの解決方法をより細かく制御でき、bareモジュールやCSS/HTMLモジュールなど、さらに多くのことを可能にすることです。

  1. ビルドツールは以下を行います
  2. 「main」モジュールつまりHTMLの<script type="module">に入れるモジュールを入手します。
  3. その依存関係を分析します:インポートとインポートのインポートなど
  4. すべてのモジュールを持つ単一のファイル(または複数のファイル、調整可能です)をビルドし、ネイティブのimport呼び出しをバンドラ関数の置き換えて動作するようにします。HTML/CSSモジュールなどの「特別な」モジュールの種類もサポートされています。
    • この処理において、他の変換と最適化が適用される場合があります
    • 到達不可能なコードが削除されます。
    • 使用されていないエクスポートが削除されます(「tree-shaking」)。
    • 現代の最先端の JavaScript 構文は、Babel を使用して類似の機能を持つ古い構文に変換できます。
    • 結果のファイルが縮小されます(スペースが削除され、変数が短い名前に置き換えられます)。

バンドルツールを使用する場合、スクリプトを 1 つのファイル(またはいくつかのファイル)にバンドルすると、それらのスクリプト内の import/export ステートメントは特別なバンドラ関数に置き換えられます。そのため、結果の「バンドルされた」スクリプトは import/export を一切含まないため、type="module" は必要なく、通常のスクリプトに入れることができます。

<!-- Assuming we got bundle.js from a tool like Webpack -->
<script src="bundle.js"></script>

つまり、ネイティブモジュールも使用できます。そのため、ここでの Webpack は使用しません。後で構成できます。

要約

要約すると、主なコンセプトは次のとおりです。

  1. モジュールはファイルです。import/export を実行するには、ブラウザに <script type="module"> が必要です。モジュールにはいくつかの違いがあります。
    • デフォルトでは遅延。
    • インラインスクリプトで非同期が機能します。
    • 別のオリジン(ドメイン/プロトコル/ポート)から外部スクリプトを読み込むには、CORS ヘッダーが必要です。
    • 重複する外部スクリプトは無視されます。
  2. モジュールには独自のローカルなトップレベルスコープがあり、import/export を介して機能を交換します。
  3. モジュールは常に use strict を使用します。
  4. モジュールコードは一度しか実行されません。エクスポートは一度作成され、インポータ間で共有されます。

モジュールを使用する場合、各モジュールが機能を実装し、エクスポートします。その後、必要に応じて import を使用して直接インポートします。ブラウザはスクリプトを自動的に読み込んで評価します。

本番環境では、Webpack などのバンドラを頻繁に使用して、パフォーマンスやその他の理由でモジュールを一緒にバンドルします。

次の章では、モジュールの他の例と、エクスポート/インポートする方法について詳しく説明します。

チュートリアルマップ