JavaScriptのモジュールシステムの歴史と現状

More than 1 year has passed since last update.

社内向け資料。自分が書いたコードを説明するために資料作る羽目になった。

昔のことはうろ覚えで雰囲気で書いてる部分もあるので、そこらへん勘弁。

古の時代(~2010)

前提としてJavaScriptは名前空間がwindowの一つしかない。

昔Prototype.jsがあった。もうみんな忘れたけどあの時期はプリミティブなオブジェクトのprototypeを生やしまくって、それが衝突しまくってprototype良くない的な雰囲気が生まれたり生まれなかったりした。

その反省があってか(歴史的に若干微妙な気がするが) jQueryは名前空間を一つに集約した。いわゆる jQueryPlugin は、jQueryのプロトタイプにヘルパを生やしまくっていた。グローバルを汚すのは駄目だけどjQueryの名前空間を汚すのはいいよね、ぐらいの考え。

jQuery非依存なライブラリは、「GoodParts」として、決めた名前空間を一つだけ使うのが暗黙の了解になっていた。 underscore.jsとかそういうのだと思う。

Node.js以降(2011~)

node.jsでパッケージマネージャーとファイルスコープの概念が導入された。

そのちょっと前、javaで動くjs実装のrhinoが出た辺りで、サーバーサイドっぽいことをやるためのcommon.jsという仕様を決めようという動きがあった。

ただしnode.jsのrequire関数はcommon.jsの仕様に従ってない。common.jsによればrequireは非同期関数だが、nodeのそれは同期関数だった。

//foo.js
exports.foo = 'foo';
//main.js
var foo = require('./foo')
console.log(foo.foo); // 'foo'

または module.exports

//foo.js
module.exports = 'foo';
//main.js
var foo = require('./foo')
console.log(foo); // 'foo'

node.jsによって他の言語(主にRuby)のライブラリが大量に移植された

パッケージ単位のエントリポイントは、package.jsonのmainプロパティで指定されたファイルになる。

{
  "name": "foo",
  "main": "index.js",
  ...
}

npmのパッケージ依存は、パッケージごとに dependencies で指定されたものがnode_modulesとして実体がダウンロードされる。devDependenciesは依存として扱われる際はインストールされないので注意。(再現したかったらnpm install --production するとよい)

npm install foo --save でpackage.jsonの依存に追加されつつインストールされる。

require.js

クライアントサイドで本来のcommon.jsの仕様を一部実装したもの(だっけ?)。モジュールの非同期ロードができる。(正直あんまり詳しくない)

define(['jquery', 'underscore'], function ($, _) {
    function a(){}; // public
    function b(){}; // private
    return a;
});

2012~に書かれたライブラリはrequire.jsとnode両対応の書き方みたいな書き方を見ることが多い。

bower

クライアント用のパッケージマネージャ。nodeにおけるnpmの、クライアントのそれという位置づけ。

npmはnodeで動かないものを登録するのが憚られたのか、jQueryプラグインの品揃えはbowerの方が多い。

個人的な肌感覚だが、browserify/webpackが浸透していくにつれて、browserify/webpackで良かったんじゃねという雰囲になりつつある。

browserify

node.js仕様のrequireを、クライアントで静的に解析してくっつけてしまえば原理的には動くじゃん!というもの。

nodeのネイティブのモジュールは基本的に読み込めないが、events.EventEmitterやfs.readFileSync等の一部が動くようにポリフィルが用意されている。これによって、ある程度気をつけて書かれたライブラリはクライアントでも動くようになった。

node.jsによって輸入されたライブラリが、browserifyを通してクライアントでも動くようになり、クライアントJSとnode.jsが曖昧になってきた。最近書かれたライブラリは、DOMにまつわるもの以外はnodeでもbrowserifyで動くと思って良い。

WebPack

基本的にbrowserifyと同じ目的のものだが、 node.js/require と require.js仕様のrequireをどちらでも扱える。

他に、webpackのほうが細かいオプションが多い。細かいオプションが多かったりするが、設定ファイルで細かく制御できてしまうがゆえにnodeとの互換がないような挙動になったりできてしまう。

browserifyの方が単機能でシンプルではあり、好みがわかれるイメージ。

ES6 Module

そんなこんなやってる間にES6(次期JavaScript)でmoduleの仕様が固まってきた。

import { returnPrivateVar, publicVar } from 'mymodule';
console.log(returnPrivateVar());

参考: ECMAScript 6 Module:現時点での仕様と利用方法 http://www.infoq.com/jp/news/2013/09/es6-modules

たぶん将来的にはブラウザかどこかで勝手に依存解決してくれるのだろうが、現在でもまだこれを実装したブラウザは存在しないため、browserifyのアプローチと同様静的解析でくっつけてしまう運用がメジャーだと思う。最近だと6to5っていうコンパイラが盛り上がってる。

6to5 · Turn ES6+ code into readable vanilla ES5 https://6to5.org/

先駆けて実装したTypeScriptは少し古い仕様を参考にしたせいかよくわからないが、ES6 moduleとは乖離がある。

module A {
    module B {
        export function C(){console.log('A.B.C')};
    }
}

import B = A.B;

ちょっとRubyっぽい。TypeScriptにはcommon.js互換のようなモードもあり、個人的にはそっちを使うようにしている。(tsc -m commonjs)

import A = require('./a')

export class B {} 
//または export = B; がmodule.exportsに相当

SourceMap

AltjsやBrowserify等のASTレベルのメタプログラミングが流行った副産物で、sourcemapという概念ができた。これはプリコンパイラがコンパイル前後のファイルの位置関係を保存してデバッガの元コードのエラー行を教える仕組み。JSにかぎらずsassとかもある。

SourceMapの多段変形が鬼門なんで、やりたいと思っても日が暮れない程度に取り組むのがよいと思う。

今はどういうコードを書けばいいか

es6 moduleを見据えつつ node.jsのrequireを使うのがいいと思う。node(io.js)でも実行でき、ブラウザでも動く。で、テストを書く際はnodeスタイルのままやるほうが遥かに簡単。

プラットフォームを限定しないほうが、後々の選択肢が増える。

(弊社では)6to5を使う必要性は、TypeScriptとCoffeeScriptを使っているので、必須ではないと思っている。

Node.jsとIo.jsどちらを使えばいいか

個人的にはコミッタが大量離脱したNodeの将来性に疑問なので、できるだけIo.jsに寄せたい。が、現時点ではまだ自分が使っているライブラリでio.jsで動かないライブラリが散見されるので、それがなくなったタイミングでio.jsに移行する。そう遠いことではないという認識。