本章では、JavaScript のモジュールを使い始めるために必要なことすべてを紹介します。
モジュールの背景
JavaScript のプログラムはとても小さいものから始まりました。初期の用途は、必要に応じてウェブページにちょっとした対話的な機能を追加する独立したスクリプト処理がほとんどであったため、大きなスクリプトは通常必要ありませんでした。そして何年かが過ぎ、今や大量の JavaScript を持つ完全なアプリケーションをブラウザーで実行することはもちろん、JavaScript を他のコンテキスト (例えば Node.js) で使うこともあります。
それゆえ近年は、JavaScript プログラムをモジュールに分割して必要な時にインポートできるような仕組みの提供が検討されるようになってきました。Node.js は長年この機能を提供しており、モジュールの利用を可能にする JavaScript ライブラリーやフレームワークも数多くあります (例えば、他の CommonJS や、AMD ベースのモジュールシステムである RequireJS など、そしてより最近では Webpack や Babel)。
良い知らせは、モダンブラウザーがモジュール機能のネイティブサポートを開始していることで、この記事がその全てです。これは良いことです。ブラウザーはモジュールの読み込みを最適化できるので、外部ライブラリーを使用してクライアント側の余分な処理やラウンドトリップを行うよりも効率的にすることができます。
ブラウザーのサポート状況
ネイティブの JavaScript モジュール機能は、import と export 文を利用します。これらに対するブラウザーの互換性は次のとおりです。
import
| デスクトップ | モバイル | サーバー | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
import | Chrome 完全対応 61 | Edge
完全対応
16
| Firefox
完全対応
60
| IE 未対応 なし | Opera 完全対応 48 | Safari 完全対応 10.1 | WebView Android 完全対応 61 | Chrome Android 完全対応 61 | Firefox Android
完全対応
60
| Opera Android 完全対応 45 | Safari iOS 完全対応 10.3 | Samsung Internet Android 完全対応 8.0 | nodejs
完全対応
13.2.0
|
| Dynamic import | Chrome 完全対応 63 | Edge 完全対応 79 | Firefox
完全対応
67
| IE 未対応 なし | Opera 完全対応 50 | Safari 完全対応 11.1 | WebView Android 完全対応 63 | Chrome Android 完全対応 63 | Firefox Android
完全対応
67
| Opera Android 完全対応 46 | Safari iOS 完全対応 11.3 | Samsung Internet Android 完全対応 8.0 | nodejs
完全対応
13.2.0
|
| Available in workers | Chrome
完全対応
80
| Edge
完全対応
80
| Firefox 未対応 なし | IE 未対応 なし | Opera 未対応 なし | Safari 未対応 なし | WebView Android 完全対応 80 | Chrome Android
完全対応
80
| Firefox Android 未対応 なし | Opera Android 未対応 なし | Safari iOS 未対応 なし | Samsung Internet Android 未対応 なし | nodejs 未対応 なし |
凡例
- 完全対応
- 完全対応
- 未対応
- 未対応
- 実装ノートを参照してください。
- 実装ノートを参照してください。
- ユーザーが明示的にこの機能を有効にしなければなりません。
- ユーザーが明示的にこの機能を有効にしなければなりません。
export
| デスクトップ | モバイル | サーバー | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
export | Chrome 完全対応 61 | Edge
完全対応
16
| Firefox
完全対応
60
| IE 未対応 なし | Opera 完全対応 48 | Safari 完全対応 10.1 | WebView Android 未対応 なし | Chrome Android 完全対応 61 | Firefox Android
完全対応
60
| Opera Android 完全対応 45 | Safari iOS 完全対応 10.3 | Samsung Internet Android 完全対応 8.0 | nodejs
完全対応
13.2.0
|
default keyword with export | Chrome 完全対応 61 | Edge
完全対応
16
| Firefox
完全対応
60
| IE 未対応 なし | Opera 完全対応 48 | Safari 完全対応 10.1 | WebView Android 未対応 なし | Chrome Android 完全対応 61 | Firefox Android
完全対応
60
| Opera Android 完全対応 45 | Safari iOS 完全対応 10.3 | Samsung Internet Android 完全対応 8.0 | nodejs
完全対応
13.2.0
|
export * as namespace | Chrome 完全対応 72 | Edge 完全対応 79 | Firefox 完全対応 80 | IE 未対応 なし | Opera 完全対応 60 | Safari 未対応 なし | WebView Android 未対応 なし | Chrome Android 完全対応 72 | Firefox Android 未対応 なし | Opera Android 完全対応 51 | Safari iOS 未対応 なし | Samsung Internet Android 完全対応 11.0 | nodejs 完全対応 12.0.0 |
凡例
- 完全対応
- 完全対応
- 未対応
- 未対応
- 実装ノートを参照してください。
- 実装ノートを参照してください。
- ユーザーが明示的にこの機能を有効にしなければなりません。
- ユーザーが明示的にこの機能を有効にしなければなりません。
使用例の紹介
モジュールの使い方を紹介するために、GitHub 上に簡単な使用例を作りました。これらは、ウェブページに <canvas> 要素を追加し、その canvas 上にいくつかの異なる図形 (と、それに関するレポート) を描画する簡単なモジュールの例です。
このような機能はあまり役に立ちませんが、モジュールの説明が明確になるように意図的に単純にしています。
注意: 使用例をダウンロードしてローカル実行する場合、ローカルのウェブサーバー上で実行する必要があります。
構造の基本的な例
最初の使用例 (basic-modules を参照) には、次のようなファイル構造があります。
index.html
main.mjs
modules/
canvas.mjs
square.mjs
注意: このガイドの使用例のファイル構造は、全て基本的に同一ですので、上記のファイル構造をよく見ることになるでしょう。
ディレクトリー modules には、次の 2 つのモジュールがあります。
canvas.mjs— canvas の設定に関する次の関数を持ちます。create()— 指定されたwidthとheightを持つ canvas を、指定された ID を持つラッパー<div>の中に作成し、そのラッパー div 自体を指定された親要素の中に追加します。戻り値は、canvas の 2D コンテキストとラッパーの ID を持つ、オブジェクトです。createReportList()— 順序なしリストを指定されたラッパー要素の中に作成し、これをレポートデータを出力するために使うことができます。戻り値は、リストの ID です。
square.mjs— 次のものを持ちます。name—文字列 'square' を内容とする定数です。draw()— 正方形を、指定された canvas 上に、指定された辺の長さ、位置、色を使って描画します。戻り値は、正方形の辺の長さ、位置、色を持つオブジェクトです。reportArea()— 指定された辺の長さを持つ正方形の面積を、指定されたレポート用のリストに書き出します。reportPerimeter()— 指定された辺の長さを持つ正方形の周囲の長さを、指定されたレポート用のリストに書き出します。
注意: ネイティブの JavaScript モジュールは、拡張子 .mjs を持つことが重要です。なぜなら、ブラウザーは JavaScript と互換性のある MIME タイプ text/javascript を持つファイルをインポートします。この拡張子を使うことにより、 "The server responded with a non-JavaScript MIME type" のような厳密な MIME タイプのチェックエラーを避けることができます。また、.mjs という拡張子は明確さ (つまり、このファイルはモジュールであり、通常の JavaScript ではないということ) や、他のツールとの相互利用性の観点からもよいことです。Google のさらに詳細なメモも参照してください。
モジュール機能のエクスポート
モジュールが持つ機能にアクセスするために最初に必要なことは、そのような機能をエクスポートすることです。これは export 文を使って行います。
最も簡単な使い方は、モジュール外部に公開したい項目の前に export をつけることです。
export const name = 'square';
export function draw(ctx, length, x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, length, length);
return {
length: length,
x: x,
y: y,
color: color
};
}
エクスポートできるものは、関数、var、let、const、および後で見ることになりますが、クラスです。これらは最上位の階層にある必要があります。例えば、関数内で export を使うことはできません。
エクスポートしたい全ての項目をエクスポートするより便利な方法は、モジュールファイルの末尾に単一の export 文を追加し、その後にエクスポートしたい機能のカンマ区切りリストを中かっこで囲んで続けることです。例えば次のようにします。
export { name, draw, reportArea, reportPerimeter };
スクリプトへの機能のインポート
モジュールから何らかの機能をエクスポートした後は、それらを使えるようにするためにスクリプトにインポートする必要があります。その最も単純な方法は次のとおりです。
import { name, draw, reportArea, reportPerimeter } from './modules/square.mjs';
import 文の後ろに、中かっこで囲まれたインポートしたい機能のカンマ区切りリストを続け、その後ろに from キーワードと、モジュールファイルへのパスを続けます。このパスは、サイトのルートからの相対パスであり、basic-modules の場合は /js-examples/modules/basic-modules です。
しかし、この例ではパスの書き方が少し異なっています。「現在の位置」を意味するドット (.) 記法を使っており、その後ろに見つけようとするファイルへのパスを続けています。これは、完全な相対パスを毎回記述するよりも短くてすむためとてもよい方法であり、URL の可搬性もあるため、サイト階層構造の異なる場所に移動させた場合でも動作するでしょう。
そのため、このようなパスは、
/js-examples/modules/basic-modules/modules/square.mjs
このように書けます。
./modules/square.mjs
このような書き方の動作している例は main.mjs にあります。
注意: モジュールシステムの中には、ファイルの拡張子やドットを省略できるものがあります (例えば '/modules/square')。このような書き方は、ネイティブの JavaScript モジュールでは動作しません。
スクリプトへ機能をインポートすると、同じファイル内で定義されているのと同じように使うことができます。次のコードは、main.mjs でインポートに続く部分です。
let myCanvas = create('myCanvas', document.body, 480, 320);
let reportList = createReportList(myCanvas.id);
let square1 = draw(myCanvas.ctx, 50, 50, 100, 'blue');
reportArea(square1.length, reportList);
reportPerimeter(square1.length, reportList);
HTML にモジュールを適用する
次に main.mjs モジュールを HTML ページに適用する必要があります。これは少し重要な点に違いがありますが、通常のスクリプトをページに適用する方法ととてもよく似ています。
最初に type="module" を <script> 要素に含めることで、そのスクリプトがモジュールであることを宣言します。
<script type="module" src="main.mjs"></script>
モジュールをインポートする先のスクリプトは、基本的に最上位のモジュールとして動作します。これを無視すると、例えば Firefox の場合は "SyntaxError: import declarations may only appear at top level of a module" (構文エラー: インポート宣言は最上位のモジュールしか使えません) というエラーが発生します。
import と export 文は、モジュールの中でのみ使うことができます。通常のスクリプトの中では使えません。
注意: type="module" さえあれば、HTML に埋め込まれたスクリプトでモジュールをインポートして使うこともできます。例えば、<script type="module"> // ここにスクリプトを書く </script> のように。
モジュールの通常のスクリプトの間のその他の違い
- ローカルでテストしようとするときは注意してください。ローカルから (つまり
file://URL を使って) HTML ファイルを読み込もうとすると、JavaScript モジュールのセキュリティ要件のために、CORS エラーが発生します。テストはサーバー経由で行う必要があります。 - また、モジュール内部で定義されたスクリプトの動作は、通常のスクリプト内部のものと異なるかもしれません。これは、モジュール内部では自動的に Strict モード が使われるからです。
- モジュールのスクリプトを読み込むときに
defer属性 (<script>の属性 を参照) を使う必要はありません。モジュールは自動的に遅延実行されます。 - 最後ですが重要なこととして明らかにしておきますが、モジュールの機能は単独のスクリプトのスコープにインポートされます。つまり、インポートされた機能はグローバルスコープから利用することはできません。それゆえ、インポートされた機能はインポートしたスクリプトの内部からしかアクセスできず、例えば JavaScript コンソールからはアクセスできません。文法エラーは開発ツール上に表示されますが、使えることを期待するデバッグ技術の中には使えないものがあるでしょう。
デフォルトエクスポートと名前付きエクスポート
これまでエクスポートした機能は、名前付きエクスポート (named export) というものです。それぞれの項目 (関数、const など) は、エクスポート時にその名前を参照されて、インポート時にもその名前で参照されます。
エクスポートの種類には、他にデフォルトエクスポートと呼ばれるものもあります。これは、モジュールがデフォルトの機能を簡単に持つことができるように設計されたもので、また JavaScript のモジュールが既存の CommonJS や AMD のモジュールシステムと相互運用できるようになります (Json Orendorff による ES6 In Depth: Modules で上手く説明されています。"Default exports" で検索してみてください)。
どのように動作するか説明するので、使用例をみてみましょう。basic-modules の square.mjs に、ランダムな色、大きさ、位置の正方形を描く randomSquare() という関数があります。この関数をデフォルトとしてエクスポートしたいので、ファイルの末尾に次の内容を書きます。
export default randomSquare;
中かっこがないことに注意してください。
または、export default を関数に追加して、次のように匿名関数のように定義することもできます。
export default function(ctx) {
...
}
main.mjs では、次のようにしてデフォルトの関数をインポートします。
import randomSquare from './modules/square.mjs';
インポートの時にも中かっこがないことに注意してください。これは、デフォルトエクスポートはモジュールごとにひとつしか作れず、randomSquare がそれであることがわかっているからです。上記は、基本的に次の簡略表現です。
import {default as randomSquare} from './modules/square.mjs';
注意: エクスポートされる項目の名前を変更するために使われる as の文法については、以下の Renaming imports and exports セクションで説明します。
名前の衝突を避ける
これまでのところ、キャンバスに図形を描く私たちのモジュールは正常に動作しているようです。しかし、円や三角形など別の図形を描くモジュールを追加しようとしたらどうなるでしょう? そのような図形にも draw() や reportArea() のような関数があるかもしれません。もし同じ名前を持つ異なる関数を同じトップレベルのモジュールファイルにインポートしようとすると、最終的に名前の衝突によるエラーが起きるでしょう。
幸いなことに、これに対処する方法はいくつかあります。それらについて、次のセクションで見ていきましょう。
インポートやエクスポートの名前を変更する
import 文や export 文の中かっこの中では、キーワード as と新しい名前を使うことで、トップレベルのモジュールでその機能を使うときの名前を変更することができます。
次の二つの例は、異なる方法ですが、同じことをしています。
// module.mjs の内部
export {
function1 as newFunctionName,
function2 as anotherNewFunctionName
};
// main.mjs の内部
import { newFunctionName, anotherNewFunctionName } from './modules/module.mjs';
// module.mjs の内部
export { function1, function2 };
// main.mjs の内部
import { function1 as newFunctionName,
function2 as anotherNewFunctionName } from './modules/module.mjs';
実際の例を見てみましょう。renaming ディレクトリでは、前の使用例と同じモジュールを使っていますが、円や三角形を描画するためのモジュールである circle.mjs と triangle.mjs も追加しています。
それぞれのモジュール内部では、同じ名前を持つ機能がエクスポートされており、それゆえそれぞれの末尾の export 文は次のように同一であることがわかります。
export { name, draw, reportArea, reportPerimeter };
これらを main.mjs にインポートするために、次のようにするとします。
import { name, draw, reportArea, reportPerimeter } from './modules/square.mjs';
import { name, draw, reportArea, reportPerimeter } from './modules/circle.mjs';
import { name, draw, reportArea, reportPerimeter } from './modules/triangle.mjs';
すると、ブラウザーは "SyntaxError: redeclaration of import name" (構文エラー: インポート名の再宣言) (Firefox の場合) のようなエラーを発生させるでしょう。
そのため、それぞれが固有の名前を持つようにするために、次のようにインポートの名前を変える必要があります。
import { name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter } from './modules/square.mjs';
import { name as circleName,
draw as drawCircle,
reportArea as reportCircleArea,
reportPerimeter as reportCirclePerimeter } from './modules/circle.mjs';
import { name as triangleName,
draw as drawTriangle,
reportArea as reportTriangleArea,
reportPerimeter as reportTrianglePerimeter } from './modules/triangle.mjs';
他の方法として、例えば次のようにすることで、モジュールファイル側でこの問題を解決することもできます。
// in square.mjs
export { name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter };
// in main.mjs
import { squareName, drawSquare, reportSquareArea, reportSquarePerimeter } from './modules/square.mjs';
これも同じように機能します。どちらのスタイルを取るかはあなた次第ですが、モジュール側のコードはそのままにしてインポート側を変更する方が、間違いなく賢明です。これは、制御できないサードパーティーのモジュールからインポートするときには、特に意味があります。
モジュールオブジェクトの作成
上記のインポート方法は正常に動作しますが、少し使いづらく冗長です。よりよい方法は、モジュール内のそれぞれの機能を、モジュールオブジェクトの中にインポートすることです。その構文は次のとおりです。
import * as Module from './modules/module.mjs';
これは、module.mjs の中にある全てのエクスポートを取得して、それらを Module というオブジェクトのメンバーとして利用できるようにすることで、独自の名前空間を持たせるような効果があります。次のようにして使います。
Module.function1() Module.function2() など
実際の使用例を見てみましょう。module-objects ディレクトリでは、また同じ例を使っていますが、この新しい構文を利用するために書き直されています。モジュール内のエクスポートは、いずれも次の単純な構文を使っています。
export { name, draw, reportArea, reportPerimeter };
一方でインポートは次のようなものです。
import * as Canvas from './modules/canvas.mjs'; import * as Square from './modules/square.mjs'; import * as Circle from './modules/circle.mjs'; import * as Triangle from './modules/triangle.mjs';
どの場合も、その指定されたオブジェクト名の配下からモジュールのインポートにアクセスできます。例えば次のようにして使います。
let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue'); Square.reportArea(square1.length, reportList); Square.reportPerimeter(square1.length, reportList);
このように (必要な箇所にオブジェクトの名前を含むようにさえすれば) コードは以前と同じように書くことができ、そしてインポートはより簡潔になります。
モジュールとクラス
最初の方で触れましたが、クラスをエクスポートしたりインポートすることもできます。これがコード上で名前の衝突を避けるもう一つの方法で、もし自分のモジュールを既にオブジェクト指向のスタイルで書いているのであれば、特に便利です。
classes ディレクトリの中には、私たちの図形を描くモジュールを ES クラスを使って書き直した例があります。例えば square.mjs ファイルでは、次のように全ての機能を一つのクラスの中に持たせています。
class Square {
constructor(ctx, listId, length, x, y, color) {
...
}
draw() {
...
}
...
}
そして、次のようにエクスポートします。
export { Square };
main.mjs では、これを次のようにインポートします。
import { Square } from './modules/square.mjs';
そして、正方形を描くために次のようにクラスを使います。
let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue'); square1.draw(); square1.reportArea(); square1.reportPerimeter();
モジュールの集約
複数のモジュールをひとつに集約させたいと思うことがあるかもしれません。依存性の階層は複数になることがあり、いくつかあるサブモジュールをひとつの親モジュールにまとめて管理を単純化したいと思うかもしれません。これは、親モジュールで次の形式によるエクスポート構文を使うことで可能です。
export * from 'x.mjs'
export { name } from 'x.mjs'
使用例は module-aggregation ディレクトリを参照してください。この例 (クラスを使った以前の例を元にしています) には、shapes.mjs というモジュールが追加されています。これは circle.mjs、square.mjs、triangle.mjs の全ての機能をひとつに集約したものです。また、サブモジュールを modules ディレクトリの中にある shapes というサブディレクトリに移動させています。つまり、この例のモジュール構造は次のようなものです。
modules/
canvas.mjs
shapes.mjs
shapes/
circle.mjs
square.mjs
triangle.mjs
それぞれのサブモジュールでは、例えば次のような同じ形式のエクスポートが行われています。
export { Square };
その次は集約を行う部分です。shapes.mjs の内部には次のような行があります。
export { Square } from './shapes/square.mjs';
export { Triangle } from './shapes/triangle.mjs';
export { Circle } from './shapes/circle.mjs';
これらは、個々のサブモジュールのエクスポートを取得して、それらを shapes.mjs モジュールから利用できるようにする効果があります。
注意: shapes.mjs の中で参照されているエクスポートは、基本的にそのファイルを経由して転送されるだけで、ファイルの中には存在しません。そのため、同じファイルの中でそれらを使ったコードを書くことはできません。
最後に main.mjs ファイルでは、全てのモジュールのクラスにアクセスするために、次のインポートを書き換えています。
import { Square } from './modules/square.mjs';
import { Circle } from './modules/circle.mjs';
import { Triangle } from './modules/triangle.mjs';
書き換え後は、次のような 1行になります。
import { Square, Circle, Triangle } from './modules/shapes.mjs';
動的なモジュールの読み込み
ブラウザーで利用できる JavaScript モジュールの最新機能は、動的なモジュールの読み込みです。これにより、全てを最初に読み込んでしまうのではなく、必要が生じたときにのみ動的にモジュールを読み込むことができます。これには明らかなパフォーマンス上の利点があります。どのように動作するのか、読んで見てましょう。
この新しい機能により、import() を関数として実行し、そのときのパラメーターとしてモジュールへのパスを指定することができます。これは次のように Promise を返し、エクスポートにアクセスできるモジュールオブジェクト (Creating a module object を参照) を使って fulfilled 状態になります。
import('./modules/myModule.mjs')
.then((module) => {
// モジュールを使って何かをする。
});
例を見てみましょう。dynamic-module-imports ディレクトリには、以前のクラスの例に基づいた別の使用例があります。しかし、今回は使用例が読み込まれたときにはキャンバスに何も描画しません。その代わり "Circle" (円)、"Square" (正方形)、"Triangle" (三角形) という 3つのボタンを表示し、それらが押されたとき、対応した図形を描くために必要なモジュールを動的に読み込んで使用します。
この使用例では index.html と main.mjs のみを変更しており、モジュールのエクスポートは以前と同じままです。
main.mjs では、それぞれのボタンへの参照を取得するために、次のように document.querySelector() を使っています。
let squareBtn = document.querySelector('.square');
そしてそれぞれのボタンに、押されたときに関連するモジュールを動的に読み込んで図形を描くためのイベントリスナーを設定します。
squareBtn.addEventListener('click', () => {
import('./modules/square.mjs').then((Module) => {
let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();
})
});
Promise が fullfilled 状態になったときにモジュールオブジェクトを返し、クラスはそのオブジェクトの部分機能であるため、Module.Square( ... ) のように Module. を追加したコンストラクターにアクセスする必要があります。
トラブルシューティング
これらは、モジュールの動作に問題があるときに助けになるかもしれないヒントです。もし他にあれば自由にリストに追加してください。
- 以前も言っているので繰り返しになりますが、
.mjsファイルはjavascript/esmという MIME タイプ (または JavaScript 互換であるapplication/javascriptのような MIME タイプ) で読み込まれる必要があり、そうでなければ厳密な MIME タイプチェックによって "The server responded with a non-JavaScript MIME type" (サーバーが非 JavaScript の MIME タイプを返しました) のようなエラーが発生するでしょう。 - HTML ファイルをローカルから (例えば
file://の URL を使って) 読み込もうとすると、JavaScript モジュールのセキュリティ要件によって CORS エラーが発生するでしょう。動作検証はサーバー経由で行う必要があります。GitHub は.mjsファイルを正しい MIME 型で返すため理想的です。 .mjsは比較的新しい拡張子であり、OS によってはそれを認識しないか、何か別のものに置き換えようとしてしまうかもしれません。例えば macOS は、通知することなく.mjsファイルに.jsを追加して自動的に拡張子を隠すことがわかりました。そのため、実際にやってくるファイルは全てx.mjs.jsのようなものでした。ファイル拡張子を自動的に隠すことをオフにして、.mjsを受け入れるように設定すると問題は無くなりました。
関連情報
- Using JavaScript modules on the web, Addy Osmani と Mathias Bynens による
- ES modules: A cartoon deep-dive, Lin Clark による Hacks ブログの投稿
- ES6 in Depth: Modules, Jason Orendorff による Hacks ブログの投稿
- Axel Rauschmayer の書籍 Exploring JS: Modules