AMDとCommonJSを考える

ひとりアドベントカレンダー17日目の記事。

今回はJSのモジュール定義について紹介します。

実は、今世間一般で使われてるJavaScriptには、まともなモジュール読み込みの構文が存在しない(!)。

そういうわけなので、外部モジュールに分割されたライブラリを読み込みたいんだ!っていう時は、ユーザーランドで色々ハックして頑張らないといけないのが現状。そんなJSモジュール定義の世界では現在、AMD, CommonJSという2大シンタックスが君臨しており、それぞれが全く異なる特徴を持っている。

色々なモジュール定義

CommonJS

CommonJS。普段Nodeを書いている人は結構馴染みがあると思う。

// some-cjs-module.js
module.exports = "hello world";  
// test-cjs-module.js
var someModule = require("./some-module");  
console.assert(someModule === "hello world");  

パッとコードを読むと分かる通り、synchronousな記法でモジュールを読み込むようになっている。
実際、Nodeでこのコードを書くと同期的にsomeModuleが読み込まれるようになっていて、非同期処理とか特に考える必要がない。とても直感的!ブラウザコンテキストだとこうはいかない。

ちなみに、同期処理で読み込んでいるよというのはNodeのソースのこのあたりを読むとわかる。 https://github.com/joyent/node/blob/v0.10.34-release/lib/module.js#L473

Nodeのモジュールローダーの実装は拡張子で読み込み処理を分けるようになっていて、.jsならばファイルを読み込んでコンパイルするし、.jsonならばファイルを読み込んでJSON.parseするし、.nodeならば動的共有オブジェクトを読み込むためにC++に処理が渡る。

ちなみにこの拡張子ごとの挙動は拡張(ダジャレではない)することができて、以下のように書くとちゃんと動く。

// test-ext.js
var Module = require("module");  
Module._extensions[".hoge"] = function () {  
    console.log("hello world");
};
require("./a");  
$ touch a.hoge
$ node test-ext.js
hello world  

なんか話がそれてしまったけど、とにかくCommonJSは同期的なシンタックスで直感的に書けるのが魅力なのです。

AMD(Asynchronous Module Definition)

一方ブラウザでは、AMDというモジュール定義が席巻しているのであった。

AMDは、CommonJSとは対照的に非同期フレンドリーなシンタックスを持っている。

// some-amd-module.js
define(function () {  
    return "hello world";
});
// test-amd-module.js
require(["./some-amd-module"], function (someAMDModule) {  
    console.assert(someAMDModule);
});

なんだかコールバック関数に囲まれていると「ちゃんと順序制御されてるな〜」って安心しますよね。えっ、しない?しますよね。

ブラウザでAMDモジュールを利用したい場合、ちょっと前まではRequireJS一択な雰囲気があったんだけど、最近はwebpackとか出てきてまたよく分からなくなってきている。まあRequireJSはパフォーマンスがなあ...って思っていたので、競合が成長してくるのはとても望ましい流れだと思う。

ちなみにまたNodeの話だけど、Nodeでは標準だとAMDモジュールを読み込むことができない。一応node-amd-loaderとかあるみたいだけど、そもそもNodeでAMDスタイルのモジュールを読み込むというのはあまり主流ではない。

UMD(Universal Module Definition)

NodeでAMDモジュールが読み込めないと書いたけれども、逆にブラウザコンテキストでは基本的にCommonJSモジュールは読み込めないし、AMDもCommonJSも一長一短なところがある。また、そもそも古のライブラリだとグローバル汚染しまくるものだってあるし、実はJSモジュールというのは、まあかなりカオスな状況なんですな。

そんなモジュール定義のスタイルを統一的に書けるようにしようという試みが、UMD

UMDでは、AMDスタイル, CommonJSスタイル, そしてグローバル汚染スタイル(勝手に命名)のどの読み込み方式からでもモジュールを使えるようなモジュール定義方法をまとめている。まあ、コードを見てみましょう。

// some-umd-module.js

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.someUMDModule = factory();
  }
}(this, function () {
    return "hello world";
}));

メインは一番下のreturn "hello world";のところだけで、あとはモジュール定義のためのコード。初めて見ると何がどうなってるのか全く意味がわからないけど、モダンなライブラリはだいたいこんな感じになっているので、コードリーディングしてると慣れてくる。恐ろしい...

ES6 Modules

さて、今まで書いてきたモジュールはユーザーランドでの話だったけど、実はECMAScript 6の言語仕様にモジュール定義が入ってきている。

harmony:modules

最新のES6ドラフトではないんだけど、アーカイブとしてまとまっているのでとりあえず上のURLを参照しておくと良いと思います。

// some-es6-module.js
export var hello = "world";  
// test-es6-module.js
import { hello } from "some-es6-module";  
console.assert(hello === "world");  

やっぱり言語仕様に入っていると安心できるけど、unstableなうちからこのへんの動向をちゃんと勉強しておかないと、すぐ時代に置いて行かれそうだなあ。くわばら。

まとめ

ですますが混ざりに混ざってものすごく読みにくい文章になってしまった...orz