ES2015のモジュール管理

2017年5月9日(火)
今井 宏明
ES2015に追加されたモジュール管理の機能を紹介し、現行のブラウザから使用する方法を解説する。

ES2015のモジュール管理

今までのJavaScriptには言語レベルでのモジュール管理機能がなく、ライブラリとして提供されているのみでした。それが遂に、ES2015では言語仕様レベルでモジュール管理に関する機能が追加されました。まだ実装されているブラウザはありません(2017年4月現在)が、トランスパイラを導入すれば使用することは可能なので、この機会に学んでいきましょう。

CommonJSとは

ES2015のモジュール管理を学んでいく前に、今までのJavaScriptにおけるモジュール管理がどのように行われてきたかという背景を理解していきましょう。

元々JavaScriptはブラウザ上で動く言語として開発されていました。そのため、サーバーサイドなどのWebブラウザ以外で動作させるためのAPIがなく、仕様についても存在していない状態でした。そこで、この仕様を策定するために発足したプロジェクトがCommonJSプロジェクトです。

CommonJSではPromiseやファイルシステムに関するものなど様々なAPIの仕様検討がされていましたが、同様にモジュール管理についてもその仕様が検討されてきました。CommonJSのモジュール管理は「exportした値や関数をrequireして使用することができる」というものです。

Modules/1.1.1 - CommonJS Wiki

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種類の方法があります。

名前付きエクスポート/インポート

関数や変数に指定した名前を使用してエクスポートすることができます。「変数を定義したタイミング」や「定義済みの関数や変数を後からまとめて」といったように、任意のタイミングでモジュールをエクスポートすることができます。

リスト1:名前付きエクスポート

// ---lib.js---
export const title = 'ES Modules'
function square(x) {
  return x * x
}
const num = Math.E + Math.PI
export { square, num }

エクスポートしたモジュールは、下記のようにインポートして使用できます。

リスト2:名前付きインポート

// ---app.js---
// 指定した名前のみインポート
import { square, num } from './lib'

console.log(square(4)) // 16
console.log(num) // 5.859874482048838

上の例では名前を指定してインポートしていましたが、エクスポートしているモジュール全体をインポートすることも可能です。

リスト3:エクスポートしたモジュール全体をインポート

// ---app.js---
import * as lib from './lib'

console.log(lib.square(4))
console.log(lib.num)

デフォルトエクスポート

モジュール1つに対して1つのメンバーのみ、名前を指定せずにエクスポートができます。

リスト4:デフォルトエクスポート

// ---lib.js---
export default function (x) {
  return x * x
}

デフォルトエクスポートでは、インポートの際に自由に名前を設定できます。

リスト5:名前を設定

// ---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の仕様に沿ってモジュール化した場合、存在しないバインディングを読み込んでも実行時までエラーかどうかがわからない状態でした。

リスト6: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を使用した場合、実行する以前にシンタックスエラーとなるためエラーを検知しやすくなります。

リスト7: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した場合はシンタックスエラーとなります。

リスト8: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が提供されています。しかしこれは利用者が明示的に書く必要があるため、うっかり記述を忘れてしまうと思わぬエラーにつながってしまうことがあります。

リスト9:ES5でありがちな記述忘れミス

// 本来であれば'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として動作するため、このようなミスを防ぐことができます。

リスト10:ES2015のModule code

// 明示的に'use strict'は宣言していない
var heroine = {name: 'Alice', age: 7}

// 常にStrict modeとして動作するためエラー
with(heroine) {
  console.log(name)
  console.log(age)
}

通常のScriptとしてJavaScriptを実行した場合、トップレベルで宣言した変数はグローバルに定義されてしまうため、他のスクリプトやライブラリから変数が上書きされてしまう危険性がありました。しかしES2015のModule codeではそのモジュール内のスコープでの変数宣言となるため、外部からの変数の上書きを気にする必要がなくなります。

リスト11:通常のScriptとES2015の比較

// 通常の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がデフォルトで有効になるため、意図しない変数のグローバル化を防ぐことができます。

リスト12:通常のScriptとES2015の比較。変数の意図しないグローバル化

// 通常の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」として策定が進んでいます。

JavaScript Loader Standard

WHATWGでは実装に向けたロードマップが設定されており、一つ目のマイルストーンとして下記の項目を決めようと検討が進んでいるようです。

  • 参照時の名前解決(相対パス、絶対パス)
  • フェッチ処理
  • <script type="module">による読み込み
  • メモリ参照

Loader Spec: The Road Ahead

<script type="module">による読み込みなど、実装が進んでいるものもいくつかありますが、現時点(2017年4月)では仕様が決まりきっていない状態のようです。

Adding JavaScript modules to the web platform - The WHATWG Blog
WHATWG Open issues - Milestone0

リクルートテクノロジーズ ITマネジメント統括部 オフショアソリューション部所属

2015年4月中途入社。前職はSIerにてWebサービスやアプリの開発からインフラ構築まで幅広い業務を担当。リクルートテクノロジーズ入社後はオフショアマネジメントを経てフロントエンドチームへ参画。週末は子育てエンジニア。ラーメンは毎日食べても飽きない。

連載バックナンバー

Web開発技術解説
第6回

ES2015のモジュール管理

2017/5/9
ES2015に追加されたモジュール管理の機能を紹介し、現行のブラウザから使用する方法を解説する。
開発言語技術解説
第5回

ES2015が備えるモダンな非同期処理

2017/3/28
ES2015に追加された非同期処理の新しい記述方法について学ぶ。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています