ES2015のモジュール管理
ES2015のモジュール管理
今までのJavaScriptには言語レベルでのモジュール管理機能がなく、ライブラリとして提供されているのみでした。それが遂に、ES2015では言語仕様レベルでモジュール管理に関する機能が追加されました。まだ実装されているブラウザはありません(2017年4月現在)が、トランスパイラを導入すれば使用することは可能なので、この機会に学んでいきましょう。
CommonJSとは
ES2015のモジュール管理を学んでいく前に、今までのJavaScriptにおけるモジュール管理がどのように行われてきたかという背景を理解していきましょう。
元々JavaScriptはブラウザ上で動く言語として開発されていました。そのため、サーバーサイドなどのWebブラウザ以外で動作させるためのAPIがなく、仕様についても存在していない状態でした。そこで、この仕様を策定するために発足したプロジェクトがCommonJSプロジェクトです。
CommonJSではPromiseやファイルシステムに関するものなど様々なAPIの仕様検討がされていましたが、同様にモジュール管理についてもその仕様が検討されてきました。CommonJSのモジュール管理は「exportした値や関数をrequireして使用することができる」というものです。
CommonJSで策定されたこれらの仕様はNode.jsなどへ取り込まれていき、サーバーサイドでのモジュール管理の標準仕様となっていきました。現在では、このCommonJS仕様のモジュール管理機能をクライアントサイドのブラウザでも使用するために、BrowserifyやWebpackを利用してCommonJSの仕様をブラウザ上で実行できる状態に書き換えて使用できるようになっています。
AMD(Asynchronous Module Definition)
AMDとは、モジュールを非同期で読み込むためのAPI仕様です。もともとCommonJSはサーバーサイド向けに考えられた仕様なので、ブラウザでの動作は考慮されていませんでした。そこでAMDではブラウザで実行されることを考慮し、非同期での読み込みや依存関係の解決に対応しています。
AMD - Asynchronous Module Definition
これらの仕様を元に実装されたモジュールローダーがRequire.jsです。Require.jsを使用することで、事前のビルドを必要とせずにブラウザ上で非同期にモジュールを読み込むことができるようになります。
しかし、Webサイトを提供する際にパフォーマンスを考慮すると、リクエスト数を削減するためにJavaScriptファイルをまとめることとなり、どうしても事前のビルドが必要となってきます。事前にビルドを行うとAMDの恩恵を受けられなくなるため、現在ではAMD自体はあまり使用されなくなってきています。
ES Modules とは
前述したように、CommonJSやAMDによってモジュール管理の仕様が検討されてきましたが、あくまでもサードパーティーとしての仕様であり、実行環境でも公式にはサポートされていない状態でした。
そこでES2015ではモジュール管理について宣言的で静的なシンタックスを定義し、JavaScriptエンジンや各ツールに対してフレンドリーな構文を言語レベルで提供していくことで標準化や最適化を図るという目的で、「ES Modules」という仕様が策定されました。
それでは、実際にどのようなシンタックスでモジュールをエクスポート/インポートしていくかを解説します。
ExportとImport
export文を使用することで、関数やオブジェクトをエクスポートすることができます。エクスポートしたモジュールは、import文により使用することができます。エクスポート/インポートには下記の2種類の方法があります。
名前付きエクスポート/インポート
関数や変数に指定した名前を使用してエクスポートすることができます。「変数を定義したタイミング」や「定義済みの関数や変数を後からまとめて」といったように、任意のタイミングでモジュールをエクスポートすることができます。
// ---lib.js--- export const title = 'ES Modules' function square(x) { return x * x } const num = Math.E + Math.PI export { square, num }
エクスポートしたモジュールは、下記のようにインポートして使用できます。
// ---app.js--- // 指定した名前のみインポート import { square, num } from './lib' console.log(square(4)) // 16 console.log(num) // 5.859874482048838
上の例では名前を指定してインポートしていましたが、エクスポートしているモジュール全体をインポートすることも可能です。
// ---app.js--- import * as lib from './lib' console.log(lib.square(4)) console.log(lib.num)
デフォルトエクスポート
モジュール1つに対して1つのメンバーのみ、名前を指定せずにエクスポートができます。
// ---lib.js--- export default function (x) { return x * x }
デフォルトエクスポートでは、インポートの際に自由に名前を設定できます。
// ---app.js--- import lib from './lib' console.log(lib(4)) // 16
名前付きエクスポートでは、インポートする際にエクスポートされている値や関数の名前を知っている必要がありましたが、デフォルトエクスポートの場合は名前を気にする必要はありません。モジュールが細かくファイル分割されることになりますが、それによってモジュール自体のメンテナンス性も向上し、使い方もシンプルになります。
前述した2種類のエクスポート/インポートはどちらも実現できることは同じですが、上記の効果が見込めるデフォルトエクスポートを使用してモジュールを管理していくことをお勧めします。
Effective JavaScriptの著者のDavid Hermanも「The syntax should still favor default import.(デフォルトインポートの構文をお勧めする)」と言っています。
ModuleImport - ECMAScript Discussion Archives
ES Modulesを使用することによるメリット
ES2015では、モジュールは静的な宣言として定義されています。宣言的で静的な構文を提供することで、下記のようなメリットが期待できます。
- 実行する前に構文レベルでエラーを検知することができる
- 最適化が容易になる
例えばCommonJSの仕様に沿ってモジュール化した場合、存在しないバインディングを読み込んでも実行時までエラーかどうかがわからない状態でした。
// ---lib.js--- function square(x) { return x * x } exports.square = square // ---app.js--- var triangle = require('./lib').triangle console.log(triangle) // undefined
ES Modulesを使用した場合、実行する以前にシンタックスエラーとなるためエラーを検知しやすくなります。
// ---lib.js--- export function square(x) { return x * x } // ---app.js--- import { triangle } from '.lib' // Syntax Error, triangle is not found.
また、Export/Importは静的に解釈される必要があるため、モジュールのトップレベルに記述しなくてはいけません。下記のように、条件文の中でモジュールをExport/Importした場合はシンタックスエラーとなります。
if(status){ export default 'app' // SyntaxError import lib from './lib' // SyntaxError // 'import' and 'export' may only appear at the top level }
このように静的な構文とすることで、実行前の時点でモジュールの依存関係がわかるようになり、エラーについてもシンタックスエラーとして検知することが容易になります。
静的な構文を提供することによって、エラー検知だけではなくJavaScriptエンジンやツールの最適化を図ることができます。例えばMicrosoft Edgeではモジュールが静的に宣言できることによって、エクスポート/インポートしたオブジェクトの最適化を実行前に行えるので、ブラウザ自体のパフォーマンスを改善することができています。
Previewing ES6 Modules and more from ES2015 ES2016 and beyond
後述するビルドツールのWebpackでは、ES Modulesで記述されたコードに対して「Tree Shaking」という仕組みが提供されています。Tree Shakingとは直訳すると「木を揺らすこと」ですが、実際には「使用されていないコードを削ぎ落とす」という意味になります。ES Modules形式で書かれたコードをbundleする際に、「exportはされているけれどどこからもimportされていない」ようなコードを削除できます。これによりビルドされるJavaScriptファイルの容量が削減されるので、システムのレスポンスタイムの改善も見込めます。
モジュール管理を静的構造にすることでのメリットは下記の「16.8.2 Static module structure」を参照してください。
Exploring ES6 - 16.8.2 Static module structure
Module code
ES2015では、「Module code」と呼ばれるソースコードのタイプが追加されました。ES Modules形式で記述しモジュールとして読み込まれるソースコードはModule codeとして解釈されます。Module codeでは下記の仕様が定義されています。
- モジュールとして読み込まれたコードは常にStrict modeとして動作する
- トップレベルでの変数はグローバルではなくモジュール内のローカル変数として定義される
- トップレベルでのthisがWindowではなくundefinedになる
ES5ではより安全にコーディングするためにStrict modeが提供されています。しかしこれは利用者が明示的に書く必要があるため、うっかり記述を忘れてしまうと思わぬエラーにつながってしまうことがあります。
// 本来であれば'use strict'を記述してStrict modeで動作する想定 var heroine = {name: 'Alice', age: 7} // with が動作してしまう with(heroine) { console.log(name) // Alice console.log(age) // 7 }
一方ES2015では、Module codeは常にStrict modeとして動作するため、このようなミスを防ぐことができます。
// 明示的に'use strict'は宣言していない var heroine = {name: 'Alice', age: 7} // 常にStrict modeとして動作するためエラー with(heroine) { console.log(name) console.log(age) }
通常のScriptとしてJavaScriptを実行した場合、トップレベルで宣言した変数はグローバルに定義されてしまうため、他のスクリプトやライブラリから変数が上書きされてしまう危険性がありました。しかしES2015のModule codeではそのモジュール内のスコープでの変数宣言となるため、外部からの変数の上書きを気にする必要がなくなります。
// 通常のScriptとして実行した場合の変数宣言 var heroine = {name: 'Alice', age: 7} // グローバル変数として宣言される console.log('heroine' in window) // true // ES2015のModule codeでの変数宣言 var heroine = {name: 'Alice', age: 7} // モジュール内のローカル変数として宣言 console.log('heroine' in window) // false
また「var」の付け忘れやタイプミスで変数を指定してしまった場合、通常のScriptとして実行した場合はグローバルに変数が宣言されていましたが、Module codeではStrict modeがデフォルトで有効になるため、意図しない変数のグローバル化を防ぐことができます。
// 通常のScriptとして実行した場合の変数宣言 title = 'Alice in Wonderland' // 意図せずに変数がグローバル化されてしまう console.log('title' in window) // true // ES2015のModule codeでの変数宣言 title = 'Alice in Wonderland' // Strict modeのためエラー
このようにモジュールとして宣言したコードは、スコープがモジュール内に閉じています。スコープが限定的であるので、すでに動作しているプログラムにモジュールを導入する場合でも安全にES Modulesの機能を利用できます。
Module Loaders
ここまでES2015のモジュール管理の定義について説明してきました。これらを使用するためにはモジュールを読み込む必要がありますが、ES2015には「どのようにモジュールを読み込むか」といったModule Loaderの仕組みは定義されていません。現在ではModule LoaderについてはECMAからは切り離され、WHATWGによって「JavaScript Loader Standard」として策定が進んでいます。
WHATWGでは実装に向けたロードマップが設定されており、一つ目のマイルストーンとして下記の項目を決めようと検討が進んでいるようです。
- 参照時の名前解決(相対パス、絶対パス)
- フェッチ処理
- <script type="module">による読み込み
- メモリ参照
<script type="module">による読み込みなど、実装が進んでいるものもいくつかありますが、現時点(2017年4月)では仕様が決まりきっていない状態のようです。
Adding JavaScript modules to the web platform - The WHATWG Blog
WHATWG Open issues - Milestone0