@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Rails 7 はユニバーサルJavaScriptモジュールの夢を叶えるか?

2009年に発表されたCommonJSの夢。この夢が叶えられるかもしれない時がきています。それもJavaScriptではないRuby on Railsによってです。

JavaScriptのライブラリ管理の歴史を紐解いてみましょう。

モジュール前夜

jQueryによるプラグイン管理がありました。jQueryの提供するAPIを使ったプラグインjQueryのユーザが使える仕組みです。 プラグインリポジトリもありました。 プラグイン間の依存関係を表現したり解決したりする機能はありませんでした。

最初のjQueryプラグインが作成されたのが2006年*1です。 この頃から2009年頃までは、JavaScriptライブラリの主戦場はブラウザでした。

Node.jsとCommonJSの登場

2009年の JSConf.euにてNode.jsが発表されました*2。同時にKevin Dangoor さんがCommonJSも発表しました*3。発表のアブストラクトにはこんな文が入っています。

Imagine a server-side webapp that runs equally well in Rhino, SpiderMonkey and v8. We're getting there. Even better, those apps can easily share modules between the browser and the server, which is something you don't get in other languages.

CommonJSは元々ServerJSという名前でNode.js向けのモジュール定義APIを想定していました。ここで言うモジュールは依存関係を定義できるライブラリだと考えて下さい。CommonJSと名前を変えたのは、ブラウザとサーバーでモジュールを共有することを目指したためです。この願いは12年後の現在、必ずしも叶えられていません。

これまでどんな挑戦がなされてきたか振り返ってみましょう。

AMDとRequireJS

2010年頃から、RequireJSはあったようです*4。 RequireJSはAMDという形式で書かれたJavaScriptのモジュールをブラウザから読み込むためのモジュールです。 AMDはCommonJS互換を目指したものではありましたが、Node.jsのモジュールとして使える物ではありませんでした。

それほど広くは流行らなかったようです。

バンドラーの先駆けbrowserify

2011年に、browserifyが登場します*5。 browserifyはNode.jsのモジュールをバンドルして、ブラウザで動作可能な1ファイルのJavaScriptライブラリーに変換するツールです。 現在では、同様のツールとして、webpack、esbuild、Rollup.jsなどが広く使われています。

ブラウザ向けパッケージ管理ツール Bower

2012年になると、npmのようなライブラリー間の依存関係を解決できるモジュール管理ツールとして、Bowerが生まれます*6。 Bowerはbower.jsonというファイルにモジュールの依存関係を記述できます。 CommonJSやAMDとは異なり、ファイル単位ではなく、npmパッケージのようなパッケージ単位の依存関係を解決します。

この頃からJavaScriptライブラリーの主戦場がブラウザとNode.jsに分かれたようです。 Node.jsの発表からわずか3年後です。Node.js向けのライブラリーが恐ろしい勢いで発展していったことがうかがえます。

その後、Bowerは2016年頃に使われなくなります*7。 大きな理由としては以下の物があります。

  1. ブラウザ向けのライブラリーもnpmで管理されることになった
  2. 複雑な依存関係をもつJavaScriptアプリケーションはバンドルされるようになった

Isomorphic JavaScriptUMD

2013年にAirbnbの Spike Brehm氏により、Isomorphic JavaScriptが提唱されます*8。 2009年で掲げられた夢が、再び求められます。 ここではもう少し実利として、SPAのSSRの可能性(当時SSRという言葉はありません)が挙げられています。

SPAでなくともブラウザとサーバーで同じ言語でロジックを書きたいという希望はわかります。 これをうけて、Node.jsでもブラウザでも扱えるモジュール定義方法として、UMDが提唱されます。 いつ頃提唱されたのかはよくわかりませんが、2014年にはすでにあったようです*9

現在でもCDNから利用可能なパッケージとしてUMD形式を配布しているライブラリーがあります。 例えばReactです*10

ESモジュールとUniversal JavaScript

ES2015の仕様向けにESモジュールが提案されます。 いつ頃提案されたのかはわかりませんが、2014年のBabelにはすでに変換機能が載っています*11。 Babelを使うことで、Node.js向けのライブラリーではESモジュールを使うものが出てきます。 また現在ではTypeScriptでも、拡張子を省略するなどの違いはありますが、ほぼ同じ構文を使っています。

2015年に、Isomorphic JavaScriptをUniversal JavaScriptと言い換える案が提唱されます*12。 2013年から引き続き、ユニバーサルなJavaScriptモジュールは求められていました。

ESモジュールもそれを念頭にいれて定義されたはずです。 しかし、現在でもブラウザではほとんど使われていません。 ES2015では、ESモジュールは定義されましたが、ブラウザからESモジュールを読込む方法は定義されていませんでした。

<script type="module">

ブラウザでESモジュールを読み込むためのタグが<script type="module">です。 2016年にはEdgeに実験的に実装されています*13。 2017年にはChromeSafariでも使えるようになりました*14

現時点ではあまり普及していません。 これには主に2つの問題があります。

ひとつめはNode.jsとブラウザでimportに指定する文字列が異なる点です。 Node.jsではファイルパスを指定しますが、ブラウザではURLを指定する必要があります。 どういうことでしょうか?

import { hello } from './module.js';

のような相対パスであれば問題ありません。

import React from "react";

のようにnpmのパッケージ名を指定した場合に、ブラウザからモジュールを発見する方法がありません。 Node.jsであれば、npmでインストールしたはずなので、node_modulesディレクトリから探すことができます。 ブラウザからはnode_modulesディレクトリが存在するかすらわかりません。 このためブラウザではURLを指定する必要があります。

もう1つの問題は、依存モジュールがファイルにわかれることです。 すべてのモジュールが別々のファイルになるため、ブラウザは依存モジュールの数だけHTTPリクエストを発行してダウンロードする必要があります。 多くのブラウザでは同時にダウンロードできるファイルは2つに制限されています。 ダウンロードするファイル数が2つ以上に増えると、ダウンロードを逐次行う必要があり、ダウンロードに掛かる時間は伸びます。

HTTP/2

2015年に策定されたHTTP/2では、1つのコネクションでリクエスト・レスポンスを並列化するため、<script type="module">のもつダウンロードファイル数が増える問題を解消することが期待できます。 2018年にはインターネット上の3割のサーバーがHTTP/2に対応していたようです*15

理論的にはHTTP/2で、この問題が解消するはずです。 しかし実証はされていません。 HTTP/2には次のような特徴が報告されています。

https://www.publickey1.jp/blog/21/http3web_tcptlshttp2http3fastly.html

2%のランダムなパケットロスというのは必ずしも現実のネットワーク環境に即した条件ではないのですが、パケットロスへの耐性という点でHTTP/2よりHTTP/1の方が優れていた

おそらくHTTP/2で解消すると思いますが、もしかすると万が一上手く行かない可能性もあります。 やってみないとわかりません。

Node.jsのESモジュール対応

2019年10月にNode.js 12からESモジュールがサポートされました。 バベル等でトランスパイルしなくても、Node.jsでESモジュールが使えるようになりました。 ただし既存のNode.jsのモジュールと区別できるように、拡張子を変更する必要があります。 この拡張子についてもめ、2019年まで掛かったようです*16

2015年のESモジュールの策定から2019年まで、ブラウザとNode.jsでのESモジュールの読込方法が確定するまで4年が掛かりました。この間にJavaScriptの界隈で、ユニバーサルモジュールへの期待値が下がってきたように感じます。

import maps

最後のピースがimport mapsです。 2021年3月リリースのChrome 89から使える機能です*17。 これは前述の

Node.jsとブラウザでimportに指定する文字列が異なる

問題を解決するブラウザの機能です。 Shimを使えば2020年7月から使えていたようです*18。 ブラウザ本体が正式サポートしました。

Rails 7

2021年現在、2009年の願いをかなえるためのパーツが揃いました。

  1. ESモジュール
  2. <script type="module">
  3. HTTP/2
  4. import maps

とはいえ、これらのパーツを、Webアプリケーション開発者が組み合わせるのは大変です。 「Webアプリケーションフレームワークがやってくれたら、今すぐ使えるのに」と思いますよね? その願いを叶えてくれるのがRails 7です。 Rails 7はESモジュールをどう扱うのか? - @ledsun blog で、書いたように、Rails 7はimport mapsをつかったJavaScriptモジュールの読込に対応しています。

Ruby on Railsのように人気のあるWebアプリケーションフレームワーク<script type="module">import mapsを使ったモジュールの読込をサポートしてくれれば、リリース後半年もすれば世界中で100個くらいのアプリケーションは実運用されそうです。

ブラウザがESモジュールを本当に上手く扱えるか確かめられるでしょう。 ユニバーサルJavaScriptが実現するかわかる日は、すぐそこ(12年に比べれば9ヶ月くらいはすぐです)に来ています。

俺の言いたい話はここからだ

少し過激な物言いになると思いますので、ツッコミは不要でございます。

これをやるのがDHHというのが熱いじゃありませんか。 Rails 5で使っていたSprocketsのES6への対応が遅く、ES6を使いたいRails利用者から不満があった中で、Rails 6ででてきたのがWebPackerというなんとも中途半端な物でした。 一部の方から「DHHはサーバーサイドはすごいかもしれないけど、フロントエンドではセンス無い」と揶揄されるのも見ました。 JavaScriptの人々がSSRとかSSGとか、わけのわからんチューニング(言い過ぎだと思う)でしのぎを削っている間にで、JavaScriptの人たちが10年以上望んでいたユニバーサルJavaScriptへの道を、DHHが高速道路を引いてみせる。実装されたプログラムで回答を示してくるんですよ。もうね、格好良すぎです。

参考