このポストは以下の記事を、筆者の許諾を得て意訳したものです。
JavaScript Modules: A Beginner’s Guide
何か誤りがありましたら、ご指摘いただけると幸いです。
(以下、意訳)
はしがき
もしあなたがJavaScriptの初心者だとしたら、以下の言葉は全く意味の分からないものかもしれません。
- モジュール管理 vs モジュール読み込み
- Webpack vs Browserify
- AMD vs CommonJS
等々…。
それでもJavaScriptのモジュール周りについて、苦手意識を持たないでください。モジュールに関して理解することは、Webエンジニアにとっては必須科目なのです。
このポストでは、カンタンな解説とコードサンプルによって上記のようなバズワードを解説します。このポストがあなたにとってお役に立つものでありますように。
注:簡素化のために、このポストは2つのセクションに分かれています。パート1では、モジュールとは何かということとモジュールを使う理由について説明します。パート2では、モジュールをバンドルするとはどういうことなのかについて説明し、いくつかの手法を紹介します。パート1、モジュールについて今一度説明してみる
良い作者が自らの書籍を章や節にきちんと分割するように、良いプログラマーはプログラムをモジュール単位に分割します。
本の章と同じようにモジュールは単なる言葉の集合に過ぎません。(あるいは、コードの集合や条件の集合と言ってもよいかもしれません)
しかし良いモジュールは、他とは異なる機能性を持っています。それゆえ、モジュールの入れ替え、除去、追加を行ったとしてもシステム全体を崩壊させることはないのです。
なぜモジュールを用いるのか
依存性の高いだらしのないコードベースにおいては、モジュールを用いる利点はたくさんあると言えるでしょう。以下、モジュールを使うことの利点を挙げていきます。
1. 保守性
モジュールの定義とは「自己充足しているもの」です。よく設計されたモジュールは、可能な限りコードベースの依存性を減らそうとします。その結果、依存性を増やさずにコードベースを拡張していくことができます。
モジュールを書き換えるとき、依存性が少ないほど書き換えは容易になります。
書籍の例で言えば、とある章の書き換えを考えてみてください。もし1つの章を少し変更した時、他の章もすべて変更しなければならないとしたらそれは悪夢でしょう。
2. 名前空間
JavaScriptにおいては、トップレベル関数のスコープ外で定義された変数はグローバル変数となります。(つまり、誰もがその変数にアクセスできてしまいます)このため、まったく関係のないコードがグローバル変数にアクセスできてしまい、これを”名前空間汚染”と呼びます。
全く関係してないコード同士で、グローバル変数を共有することは、もっともやってはいけないことの1つです。
後ほど、名前空間汚染を防ぐために、モジュールを利用してprivateな名前空間を作る方法を紹介します。
3. 再利用性
以前書いたコードをコピペしてしまうことは、誰だって経験があるでしょう。ユーティリティ関数をコピペしてきたと想像してみてください。
まあ動くでしょうが、もしリファクタリングする際は大変です。全てのコピペコードを書き換えなければいけないのですから。
これは大きな時間のムダです。もし何度も再利用できるモジュールがあったら便利だと思いませんか?
どうやってモジュールを結合させるのか
モジュールを結合する方法はたくさんあります。そのうちのいくつかの例を見ていきましょう。
モジュールパターン
モジュールパターンは、クラスを擬似的に再現するために使われます。(なぜならネイティブのJavaScriptではクラスをサポートしていないからです)クラスを擬似的に再現することで、1つのオブジェクトの中に関数や変数を保持しておくことができます。これはまさに、JavaやPythonのような他言語のクラス実装と似ています。
モジュールパターンを実現する方法はいくつかあります。最初の例では、匿名クロージャを使います。この例では、1つの匿名関数に全てのコードを閉じ込めています。(JavaScriptでは、関数がスコープを生成する唯一の方法です)
例1. 匿名クロージャ
(function () {
// We keep these variables private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
}());
// ‘You failed 2 times.’
このように記述することで、匿名関数はそれ自身の評価環境、つまりクロージャを持ちます。そして、ただちに評価が行われます。このおかげで、上位の名前空間から変数を隠すことができます。
この手法の良い点は、既存のグローバル変数を誤って上書きすることなく、関数内でローカル変数を使うことができます。かつ、関数内からはグローバル変数を参照することができます。
var global = 'Hello, I am a global variable :)';
(function () {
// We keep these variables private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
console.log(global);
}());
// 'You failed 2 times.'
// 'Hello, I am a global variable :)'
匿名関数を丸括弧で囲むようにしてください。なぜなら、functionという単語で始まる文は、関数宣言としてみなされるためです。(JavaScriptでは無名関数の単独宣言はできません)結果として、丸括弧が代わりに関数式を生成します。もし興味をお持ちでしたら、こちらも参照ください。
例.2 グローバルインポート
もう1つの有名な手法はグローバルインポートです。これはjQueryが採用している手法でもあります。グローバルインポートは、先ほど見たような匿名クロージャと似ていますが、パラメータとしてグローバル変数を渡します。
(function (globalVariable) {
// Keep this variables private inside this closure scope
var privateFunction = function() {
console.log('Shhhh, this is private!');
}
// Expose the below methods via the globalVariable interface while
// hiding the implementation of the method within the
// function() block
globalVariable.each = function(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
globalVariable.filter = function(collection, test) {
var filtered = [];
globalVariable.each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
globalVariable.map = function(collection, iterator) {
var mapped = [];
globalUtils.each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
};
globalVariable.reduce = function(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
globalVariable.each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
};
}(globalVariable));
この例では、globalVariableだけが唯一のグローバル変数です。この手法の利点は、グローバル変数を最上位で定義することです。これによって、可読性が増します。この点が例1の匿名クロージャに優る点です。
例.3 オブジェクトインターフェース
そしてもう1つの手法が、以下のような自己充足型のオブジェクトを使って、モジュールを生成する方法です。
var myGradesCalculate = (function () {
// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
// Expose these functions via an interface while hiding
// the implementation of the module within the function() block
return {
average: function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
},
failing: function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
}
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
ご覧のように、この手法によって私たちはどれをprivateにしてどれをpublicにするかを決定できます。コードサンプルでは、myGradesがprivateになっていて、averageとfailingがpublicになっています。
例.4 開放モジュールパターン(英語ではRevealingModulePatternでしたが、良い訳が思いつきませんでした…。)
これは例3の手法にとてもよく似ていますが、デフォルトでは全てのメソッドと変数がprivateである点が異なります。
var myGradesCalculate = (function () {
// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
};
var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
};
// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly
return {
average: average,
failing: failing
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
この例では、とても多くのことを取り入れているように見えるかもしれません。しかし実際には、モジュールパターンという意味では氷山の一角に過ぎません。以下に私が有用だと思った記事を列挙します。
CommonJSとAMD
上述したすべての手法には、1つの共通点があります。関数内のコードをラップするために1つのグローバル変数を使うことです。それによって、クロージャスコープを利用してprivateな名前空間を生成することができるのです。
それぞれの手法はそれはそれで良いものですが、デメリットもあります。
1つ目のデメリットは、読み込まれるファイルの正しい依存関係をエンジニアが知っている必要があります。例えば、プロジェクトでBackbone.jsを使っているとしましょう。Backboneのコードを読み込むために、ファイル内でscriptタグを使います。
しかし、Backbone.jsはUnderscore.jsに強く依存しているため、Undersocre.jsよりも前にBackboneのためのscriptタグを書くことはできません。
エンジニアにとって、依存性を管理してものごとを正しく整理することは頭痛の種となりえるでしょう…。
もう一方のデメリットは、それでも名前空間の衝突は起こりうるということです。例えば、もし2つのモジュールが同じ名前だったらどうでしょうか?あるいは、2つのバージョンのモジュールが存在していて、両方が必要になったときはどうでしょうか?
あなたは、こう考えるかもしれません。
グローバルな空間を利用しないモジュールインターフェースは存在しないのか?と。
幸運なことに、答えはイエスです。
CommmonJSとAMDという、よく使われている2つの有名な手法があります。
CommonJS
CommonJSは、モジュール定義のためのJavaScriptAPIを設計・実装している有志の団体です。
CommonJSのモジュールは、再利用可能なJavaScriptの部品です。exportされたあるモジュールを、他のモジュール側でrequireすることで再利用できます。もしNode.jsでの開発経験があるのなら、このフォーマットには馴染みがあるでしょう。
CommonJSを用いることで、ユニークなモジュールコンテキストにおいて、それぞれのJavaScriptファイルがモジュールを保持します。(ちょうどクロージャでラップするような感じです)このスコープ内においては、module.exports
によってオブジェクトをモジュールに晒し出し、require
によってそれらを持ち込みます。
CommonJSのモジュールを定義すると以下のようになるでしょう。
function myModule() {
this.hello = function() {
return 'hello!';
}
this.goodbye = function() {
return 'goodbye!';
}
}
module.exports = myModule;
module.exportsへと関数の参照を渡しました。これによって、CommonJSモジュール側で受け渡し処理を行ってくれます。
そして、誰かがmyModule
を使いたく鳴った時は、以下のようにrequireすればよいのです。
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'
先述のモジュールパターンに優るような、明らかなメリットがこの手法には含まれています。
- グローバル名前空間の汚染を防ぐことができる。
- 依存関係を明示することができる。
さらに、シンタックスがとてもコンパクトなので個人的には気に入っています。
もう1つ特筆すべき点は、CommonJSはサーバファーストな手法をとっており、同期的にモジュールを読み込みます。もし複数のモジュールをrequire
しなければならないとき、順番に読み込んでくれるのです。
さて、CommonJSはサーバにとっては素晴らしいものですが、残念ながらブラウザでJavaScriptを書く場合には難度が増します。Webからモジュールを取得するのは、ディスクから取得するよりも _はるか_に時間がかかるのです。モジュール読み込みのスクリプトが走っている間は、ローディングが終わるまで他の作業をストップさせてしまいます。このような振る舞いは、JavaScriptのスレッドがコードをロードするまで止まってしまうことに起因します。(この問題をどう回避するかについては、モジュールバンドルに関するパート2で話しましょう。)
AMD
CommonJSは素晴らしいものですが、もし非同期でモジュールを読み込みたい時はどうでしょうか?答えは、非同期モジュール定義(AsynchronousModuleDefinition)、略してAMDを利用することです。
AMDでのモジュール読み込みは以下のようになります。
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
解説をしましょう。define
という関数は、第一引数としてモジュールの依存関係を表す配列をとります。これらの依存関係は、バックグラウンドで読み込まれ(ノンブロッキング処理)、そしてdefine
関数が読み込まれるやいなや与えられたコールバック関数を実行します。
次に、コールバック関数は読み込まれた依存関係を引数としてとります。コードサンプルではmyModule
とmyOtherModule
ですね。これによって、関数内でモジュールを用いることができます。最後に、依存関係それ自体もまた、define
という単語を使って定義される必要があります。
例として、myModule
は以下のようになります。
define([], function() {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
今一度説明します。CommonJSとは異なり、AMDは非同期処理を利用してブラウザファーストな手法をとっています。(一方で、コード実行時の動的ローディングは好ましくないと強く信じる人々がいることも記しておきます。この点は次のセクションで触れます)
非同期以外のAMDの利点といえば、モジュールとしてオブジェクト、関数、コンストラクタ、JSONなど様々なタイプを許容している点です。CommonJSでのモジュールといえばオブジェクトのみでした。
しかし、CommonJS経由で利用可能な入出力やファイルシステムなどのサーバサイドの仕組みとAMDは合わないとも言われます。そして、シンプルなrequire
式に比べれば、AMDのfunctionでラップしたシンタックスはやや冗長だとも言われます。
UMD
AMDとCommonJSの両方をサポートしなければならない局面においては、UMD(UniversalModelDefinition)を用いることができます。
UMDは、グローバル変数定義をサポートしているものの、本質的には両者を用いるための方法を提供します。結果として、UMDのモジュールはクライアントとサーバの両方で利用可能となっています。
以下が、UMDのカンタンな例になります。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)
// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));
より多くのUMDのサンプルについては、enlightening repoを参考にしてください。
Native JS
おっと、まだついて来ていますか?迷子になっていませんよね?良かった!なぜなら、 __もう一つ__のモジュール定義方法が残っているからです。
お気づきかもしれませんが、ここまでで紹介したすべてのモジュール例は、ネイティブのJavaScriptではありません。その代わりに、CommonJSとAMDの両方のモジュールパターンをエミュレートできる方法を発案しました。
幸い、ECMAScript6にはmoduleがビルトインされました。
ES6はモジュールのexportとimportに関して、様々な可能性を提供しています。以下がその例となります。
CommonJSやAMDと比べて、ES6のモジュールの素晴らしいところは、次の両者を最高の形で実現したことです。それはコンパクトでわかりやすいシンタックスと、非同期な読み込み、これらの両者です。さらに、循環的な依存関係のより良いサポートのような利点も加わっています。(直訳)
私の気に入っているES6の仕様は、importがexportの _生きた_読み取り専用であることです。(この点をCommonJSを比べてみてください。CommonJSにおいては、importは単なるexportのコピーであり、 _生きた_ものではありません)
では、どのように動作するかを以下に示します。
// lib/counter.js
var counter = 1;
function increment() {
counter++;
}
function decrement() {
counter--;
}
module.exports = {
counter: counter,
increment: increment,
decrement: decrement
};
// src/main.js
var counter = require('../../lib/counter');
counter.increment();
console.log(counter.counter); // 1
この例では、モジュールの2度コピーしています。1つが、exportの際、もう1つがrequireの際です。
さらに、main.jsにおけるコピーではオリジナルのモジュールからは切り離されています。そのため、counterをインクリメントしても1のままでした。なぜなら、requireしたcounterという変数は、モジュールのcounter変数とは切り離されているためです。
そのため、counterをインクリメントするとモジュール内のcounterはインクリメントされますが、コピーされた方のcounterには反映されません。
コピー後のcounterをインクリメントする方法は、以下のとおり。手動で行う必要があります。
counter.counter++;
console.log(counter.counter); // 2
一方でES6は、importしたモジュールの読み取り専用を生成します。
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}
// src/main.js
import * as counter from '../../counter';
console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2
これによって、機能性を失わずして、さらにモジュールを細分化することができる点が非常に素晴らしいです。
モジュールバンドラをお楽しみに
あっという間でしたね。ざっくりとではありましたが、この解説があなたにとってモジュール理解の助けとなれば幸いです。
次のセクションでは、モジュールのバンドルについて解説していきます。その際に、中心となるトピックは以下のとおりです。
- なぜモジュールをバンドルするのか。
- バンドルの様々な手法。
- ECMAScriptのモジュールローダーAPI
- 等々。
(訳おしまい。Part2に続きます)