JavaScriptにおけるモジュールとimport/exportの使い方

2016年09月30日

カテゴリー:

JavaScriptにおけるモジュール

例えばコーポレートサイトのような普通のWEBサイトの場合、JavaScriptコード量はそれほど多くはならないないと思います。ページにいくつかのギミックを加える程度ならば1つのJavaScriptファイルに全てのコードを詰め込んだとしても特に問題は無いでしょう。

しかし、WEBアプリ開発のようにJavaScriptでゴリゴリとコードを書いていく場合、ある程度機能(役割)別にファイルを切り出して別ファイルとして管理したいところです。そのようにして関連性を持たせて切り離された一塊のコード群が「モジュール」です。

説明するまでもないのですが、別にモジュール化せずとも(やろうと思えば)それなりのアプリは作れるとは思います。しかしその先に待っているのはいばらの道です。精神衛生の面から言っても、ある程度大きなWEBアプリを作るのであれば積極的にモジュール化を検討した方が良いでしょう。適切な粒度でモジュール化することには次に示すような利点があるからです。

保守性
他のコードとの依存性を少なくすることにより、変更や拡張がしやすくなります。
(モジュールの修正が他のコードに影響しにくい)
変数名の競合を防ぐ
モジュール毎に適切な変数名(名前空間)を割り当てることで、変数名の競合リスクを減らすことができます。また、どの機能がどこに書かれているのかが把握しやすくなります。
再利用性
汎用性の高いモジュールは使い回しが効くようになります。
同じコードをコピペして使いまわしてしまうと、コードの修正が発生した場合にコピーした分だけ修正しなければなりませんが、1つのモジュールを再利用すれば1か所の修正のみで済みます。

おそらく大抵の言語にはモジュールを扱うための仕組みが用意されています。例えばJavaにはパッケージ(package)があり、C#にはクラスライブラリといったモジュール化のための仕組みがあります。しかし、JavaScriptはこれまで言語仕様としてモジュールをサポートしていませんでした。

もともとJavaScriptは「WEBページにちょっとしたギミックを加える」程度の用途を想定して設計されたものであるため、コード全体の規模としてもかなりコンパクトになることが予想されたからだと思います。

しかしJavaScriptは途方もなく成長してきました。最近ではシングルページアプリケーションのようにJavaScriptでかなり複雑なWEBアプリが開発されることも多くなってきています。この規模になるともはやモジュール化なしでは開発が極めて困難になってきます。そのためJavaScriptでは(言語仕様としてのモジュールの仕組みは用意されていないので)コーディングテクニックで疑似的にコードをモジュール化するような方法がとられていました。この手法がいわゆる「モジュールパターン」と呼ばれるコーディングパターンです。

以下はモジュールパターンの一例です。

'use strict';

var modules = modules || {};

modules.calculator = (function () {
  var add = function (a, b) {
    return a + b;
  };

  return {
    add: add
  };
}());

細かい解説はここでは割愛しますが、上記コードではmodulesという名前空間に calclator というオブジェクトを作成し addメソッドを追加しています。

このコードを例えば、「calculator.js」として保存し、HTMLに読み込ませることで、モジュール外部のスクリプトから次の様にして使用することができます。

'use strict';

modules.calculator.add(1, 2); //3

疑似的とはいえ、このようにスクリプトをモジュール化することでかなりコード全体の見通しを良くすることができます。また、上記ではグローバル変数として「modules」という名前のオブジェクトを1つ作成していますが、あらゆるモジュールをこのmodulesオブジェクトに登録(modulesオブジェクトのプロパティにする)することで、名前空間の汚染も最小限にすることができます。

モジュールパターンの限界

上記のモジュールパターンは、これはこれで沢山のメリットを享受することができ、JavaScriptのコードを体系的に作り上げていくためのとても良い手法だとは思います。(私も良く使うパターンです。)

しかし、このモジュールパターンをもってしても解決できない問題を次に挙げます。

モジュールの依存関係の解決
あるモジュールが別のモジュールに依存している場合、正しい順序でスクリプトを読み込ませる必要があります。(モジュールを使う人が、モジュールの依存関係を正しく把握している必要がある。)
グローバルスコープの汚染
モジュールパターンを使用することで劇的にグローバルの使用を抑えることができますが、それでもグローバルは汚染されます。
モジュールパターンでは1つ以上のグローバル変数を使う必要があるからです。

これらは、はじめはさほど問題にはならないかもしれませんが、WEBアプリの規模が大きくなればなるほど煩わしい問題として表面化してくるでしょう。

CommonJSやAMDの登場

上で述べたようなモジュールパターンの問題を解決するための方法がCommonJSやAMDです。
CommonJS、AMDについての詳細な説明は本稿のテーマではないので割愛しますが、ざっくり言うと「JavaScriptをもっと様々な用途で使えるようにするための仕様」で、ECMAScriptではなく任意団体により策定、標準化が進められています。

当初、JavaScriptはWEBブラウザ上で動作することを前提に設計されていたので、他の実行環境(サーバーサイドなど)で使おうにも、他の言語が標準的に持ち合わせているようなAPIが色々と足りません。足りないAPIを補うために各々が好き勝手に独自APIを定めてしまうと当然ながら仕様が乱立し、同じ機能を使うにしても実行環境に合わせて実装していくはめになります。これは一昔前のクロスブラウザ対応のようなもので、非生産的で開発者を苦しめるだけのものです。

そこで、こういったAPIの仕様や実装方法を統一するために登場したものがCommonJSやAMDといった拡張仕様です。

module.exports.talk = function () {
  console.log('My name is Taro!');
};
var taro = require('taro');
taro.talk(); //My name is Taro!

ただ、これらの仕様はECMAScript標準とは別の枠組みで策定されたものなので、当然ながらそれらをそのままWEBブラウザが解釈することは出来ません。

CommonJSやAMDの仕様に沿って作成されたモジュールをブラウザで利用できるようにするには、BrowserifyやRequireJSといったものが必要になります。

BrowserifyはCommonJSの仕様に準じて作られたモジュールバンドラです。
ごく簡単に説明すると、モジュールファイルとモジュールを使う側のファイルを事前にブラウザが解釈可能な1つのJavaScriptファイルに結合(バンドル)し、HTMLファイル側からは結合後のJavaScriptファイルを参照させるというアプローチです。

対してRequireJSはAMDの仕様に準じたモジュールを読み込むためのフレームワークです。Browserifyのような事前コード変換方式ではなく、ページの実行時にモジュールの依存関係を解決させます。HTMLファイル側からはRequireJS本体と基点となるJSファイル(モジュールを使うJavaScriptファイル)を指定し、このJSファイルから参照されているモジュールファイルを非同期で順次ロードしていくというアプローチです。

CommonJS、AMDのメリット

CommonJSやAMDに準じて作成されたモジュールファイルは、モジュール自身がどのモジュールに依存しているのかを実装するので、モジュールを使う際に依存関係を意識する必要が無くなります。また、これらのモジュールは専用の名前空間を必要としないので、名前空間の汚染を完全に防ぐことができます。

ES2015からは標準採用された

上述したように、これまではモジュール・パターンのような「疑似モジュール化」のような手法や、CommonJSやAMDといった「標準ではない」仕様によって実装されてきたJavaScriptのモジュールですが、ついにES2015(ES6)でJavaScriptの標準仕様として採用されました。

モジュール・パターンやCommonJS/AMDはいずれも有益ですが、ネイティブJavaScriptのみで完全なモジュール化が実現できることを考えると、今後はこちらが使われていくようになるでしょう。

export var name = 'Taro';
export var age = 20;
export var talk = function () {
  alert('I am Taro');
};
import {name} from 'module.js';
 
console.log(name); //Taro

ES2015のモジュール化の強み

ES2015でのモジュールの仕組みは、ネイティブJSだけで実現できるというだけでも十分なメリットではあるのですが、後発の仕様ということもあり既出のモジュール化の仕組みの「いいとこどり+α」で実装されている点があげられます。

【ES2015モジュールの特徴】

  • コンパクトなシンタックス
  • モジュールの非同期読み込み
  • 1ファイル1モジュール
  • 強制strictモード(’use strict’;不要)
  • 循環参照
  • モジュールの生きた読み取り専用

上記に挙げたメリットについては次の記事で詳しく解説されていましたのでここでは触れませんが一度目を通しておくと良いかもしれません。
[意訳]初学者のためのJavaScriptモジュール講座 Part1 – Qiita

モジュールを使うためには?

残念ながら現時点ではES2015のモジュールをそのまま解釈できるブラウザはまだ存在しません。(ES2015の新機能の中では最もブラウザへの実装が遅れそうな機能です。)もちろん「現時点」でのお話なので将来的には使えるようになるはずですが。
今ES2015のモジュールを利用するためには、少なくとも2つのステップが必要になります。

トランスパイル

1つはトランスパイラ(Babelなど)を使い、ES2015で書かれたJSコードを、(ES5ベースの)CommonJSやAMD等のフォーマットとして変換する。

バンドル

2つめは、トランスパイラによって書き出されたコードをモージュールの依存関係を見ながら1つ(または複数)のJavaScriptファイルへとつなぎ合わせる

ブラウザからは、バンドルされたファイルを読み込みます。

ただ、JSファイルの編集の度に上記のようなプロセスを実行するのはかなり手間なので、実際の現場ではこれらのタスクを自動化させるためのツール(Gulpなど)を使ったりすることが多いと思います。

モジュールファイルの作成(export)と読み込み(import)

さて、ようやく本題といった感じですが、ここからごく簡単な例をもとにES2015でモジュールの作成と読み込みをやってみましょう。

モジュールファイル側では「export」キーワードを使って変数をモジュール外部に公開することができます。

例えば、次に示す様なモジュールファイルがあるとします。この時点ではまだ「export」キーワードは使われていないので、モジュールからは何も公開されない、つまり何の意味もないファイルです。

var name = 'Taro';
var age = 20;
var talk = function () {
  alert('I am Taro');
};

このファイルで定義されている全ての変数(name, age, talk)を外部に公開する場合は以下の様になります。

export var name = 'Taro';
export var age = 20;
export var talk = function () {
  alert('I am Taro');
};

または、1つのexport文にまとめて以下の様に書くこともできます。

var name = 'Taro';
var age = 20;
var talk = function () {
  alert('I am Taro');
};

export {name, age, talk};

こうしてエクスポートされた変数は「名前付きエクスポート」といい、インポートする際に対応する変数名を指定して値を取り出すことができます。実際にこのモジュールを読み込むインポート側のファイルでは次の様に記述します。

import {name} from 'module.js';

console.log(name); //Taro

モジュールを利用する側のファイルではimport文を使用します。上記例では、先ほど作成したモジュールファイル(module.js)からname変数をインポートしています。モジュールファイルの指定には、インポートする側のファイルからの相対パスで指定できます。

また、次の様に1つのimport文で複数の変数を一括してインポートすることも可能です。

import {name, age, talk} from 'module.js';

console.log(name); //Taro
console.log(age); //20
console.log(talk()); //I am Taro

さらに、モジュールファイル1つにつき、1つだけデフォルトエクスポートを定義することができます。
デフォルトエクスポートとは、そのモジュールの主要なエクスポート値であり、エクスポート/インポートする際に変数名を必要としません。変数名を必要としませんので、デフォルトエクスポートを行う際にはvar, let, constといった変数宣言用のシンタックスは不要です。defaultキーワードの後に数値、文字列などのリテラルや関数、クラスを使用することができます。

export var name = 'Taro';
export var age = 20;
export default function () {
  alert('I am Taro');
};
import talk from 'module.js';

console.log(talk()); //I am Taro

上の例では、モジュールファイル(module.js)からtalkという変数を読み込もうとしていますが、そのような変数はエクスポートされていませんので、デフォルトエクスポート(ここでは関数)が変数 talk にインポートされます。

デフォルトエクスポートと、名前付きエクスポートは次のようにして一括でインポートすることもできます。

import talk, {hoge, fuga} from './export';

talk(); //I am Taro
console.log(hoge); //hoge
console.log(fuga); //fuga

import構文は非常に多くのパターンが用意されています。本稿では一般的によく使われるであろう、基本的なimport/export文の紹介のみですが、別の機会に改めて掘り下げてまとめます。

余談ですが、通常、スクリプトファイルのグローバルスコープ直下にvarで変数宣言をすると、その変数はグローバルオブジェクトのプロパティになりますが、モジュールファイルの直下にvarで変数宣言をしてもグローバルオブジェクトのプロパティにはなりません。つまり、exportされていないモジュール内の変数は外部から参照されることはありません。