2015年3月6日
2015年に向けたJavaScriptアプリケーションアーキテクチャ Part 1
本記事は、原著者の許諾のもとに翻訳・掲載しております。
私はかつて自分はアーキテクトだと名乗ったことがあります。これを裏付けるため、今やウソだらけの複雑な話を設計しなくてはならなくなっているので、ある意味これは本当のことですね。冗談はさておき、2015年を目前としてJavaScriptコミュニティのアプリケーションアーキテクチャの状況について目を向けてみるのは有益なことだと思います。合成、関数型の境界、モジュラリティ、不変データ構造、CSPのチャネルと、その他に関連するいくつかのトピックについて書いてみたいと思います。
合成
アーキテクチャのレベルでは、JavaScriptで大規模なアプリケーションを作成する方法に関してここ数年で少なくとも一つの根本的な変更がありました。機械の細かい違いにより生み出される単一指向性の データバインディング、不変データ構造と、仮想DOM (どれも興味深い問題ですね)などを除けば、多くの開発者が一つのキーコンセプトに自然に収束してきたように見えます。それが 合成 です。合成は非常にパワフルで、再利用可能な機能のパーツをまとめて、より大きなアプリケーションを”合成”することを可能にします。合成は モジュール化 、小型でテストしやすいなど、よい状態の考え方を導いてくれますから、説得しやすくディストリビューションも楽です。では、合成がNodeに対してどれくらいうまく機能するか見てみましょう。
合成のモデル。アプリケーションは再利用可能なUIの小さな部品からできていて、それらのUIも拡張され再利用された既存のモジュールとライブラリからできています。
私たちは定期的にReact”コンポーネント”について、”Ember.Component”について、Angular Directive、Polymer要素と、もちろんストレートにWebコンポーネントの要素などについて話すことがありますが、その理由の一つが「合成」について知ってもらいたいからです。これらのさまざまな特性のコンポーネント周りのフレームワークやライブラリについてあれこれ言うことはあるかもしれませんが、合成そのものが悪だという議論をするつもりはありません。注:JSフレームワークのゲーム(Dojo、YUI、ExtJSなど)の初期のプレーヤーたちは随分前から合成を絶賛していましたが、直接、その他大勢の人たちに広くこのモデルの真のパワーをきちんと理解してもらうまでには、しばらく時間がかかりました。
合成はアプリケーションが複雑化するという問題に対する、一つの解決方法です。 さてWebプラットフォームの言語は、複雑性によって引き起こされたトラブルに直接呼応するように進化しています。複雑性と言ってもさまざまですが、開発者たちが過去数年どのようにWebを構築してきたかということを見通してみると共通のパターンがあり、検討する価値のある解決方法の一つとなります。だからこそ、Webコンポーネントを重視するブラウザベンダによる承認がとても重要なことなのです。もしあなたが”当社の”バージョンを絶対使わないとしても(もちろんとてもパワフルだと反論しますが)、合成が提供する解決法を利用できるくらいまで十分に理解していただければと思います。
将来、改善が見込まれる部分は、状態同期(コンポーネントDOMとモデル/サーバの状態を同期する)の周辺と、合成の境界の実力の利用についてです。
合成の境界
Web上の合成について話しておいて Shadow DOM の境界について話さなければ不公平でしょう(もう少し私の話につきあってくださいね)。この機能については、昔ながらの汎用HTMLで構成したウィジェット間に機能的な境界を提供してくれるものだと考えるのがいいでしょう。この機能は一つのウィジェットツリーがどこで終わって、他のウィジェットツリーがどこで始まるのかを分かりやすくし、ウィジェットがページ上でリークを起こすようなトラブルを防止してくれます。また、深いウィジェット構造がコストのかかるスタイリングルールから要素を隠すのに最適なCSSセレクタはどれかを調べてくれます。
Shadowの境界は通常のDOM(”ライトな”DOM)と、Shadow DOMを分けるバリアです。
あなたはまだShadow DOMに馴染みがないかもしれませんね。これは、ブラウザがDOM要素(例えばコンポーネントを表現するものなど)のサブツリーを、該当ページのメイン文書のDOMツリーではなく、文書レンダリングに加えることを許可します。これによりコンポーネント間の 合成の境界 を作成され、iframeに頼らなくても、コンポーネントを小型で独立した固まりだと推論させてくれます。Shadow DOMを備えたブラウザは、簡単にこの境界を越えて動くことができますが、DOM要素のサブツリーの中では、今日使っているのと同じdiv、inputとselectタグを使ってコンポーネントを合成できます。 これによって、Shadow DOM内に隠されたコンポーネントの実装の詳細からページの残りを守ることができます。
大昔からスコープとスタイリングを分けるために使われてきたものやiframeを使うだけの場合と比べて、Shadow DOMはどうでしょう? 最低なだけでなく、iframeは完全に別々のHTML文書を、別のHTML文書の真ん中に挿入するように設計されています。一つのiframe(完全に分けられたコンテキスト)内にあるDOM要素へのアクセスは、デフォルトで脆く、とても手間がかかるものでした。また、このメカニズムで複数のコンポーネントをページに”挿入すること”を考えてみましょう。扱いにくく意味論的に価値がないiframeタグがマークアップに散乱していますが、いずれの場合にもiframeのホストのコンテンツ用の別のURLが見つかります。合成の境界のためのShadow DOMを使用するコンポーネントは、ネイティブのHTML要素と同じくらい消費や修正が簡単です(または、そうあるべきです)。
あえて反論してみると、合成はプレゼンテーション層で生じるべきではないと感じる人もいれば(あなたの好みではないかもしれませんね)、生じるべきだと感じる人もいるでしょう。Shadow DOMを使わずにコンポーネントの合成が絶対できると言うことから始めましょう。しかしそうするためには、コンポーネントの境界について、それを実現するために抽象化を追加して、極端に規律を守らなければいけません。Shadow DOMはプラットフォームの抽象化の方法を与えてくれます。
一方で、これは元々Chromeで可能なことですし、 将来的に 他のブラウザでもできるようになります。既存のアプローチがサポートに乗り出すのを見るのはわくわくします。例えばReactはShadow DOM向けのサポートを 追加中 で、近々このサービスを展開しようとしています。Polymerは既にサポートを開始しており、EmberとAngularもこのエリアについて検討されています。
コンポーネントのメッセージング
コンポーネント間のコミュニケーションはどうなっているでしょう? 分離したモジュラコンポーネントで作業しているなら、いくつかの選択肢があります。
コンポーネントAPIを経由した直接参照は(非常にシンプルなもので作業しない限り)好ましくありません。他のコンポーネントの特定のバージョンに直接的な依存をするからです。 もしAPIが大きく変更されれば、対価を払ってアップグレードするか破損に対処しなければいけません。また他の方法として、グローバルまたはインコンポーネントな従来型のイベントシステムを使用することもできます。親子関係のないコンポーネント間でコミュニケーションを取る必要があれば、イベント+サブスクリプションはいまだにポピュラーな方法です。実際に、親子関係のないコンポーネント間で単に props を渡すだけの場合には、Reactはこのアプローチを推奨しています。Angularはコンポーネントのコミュニケーションのためにサービスを使用しており、Polymerにはカスタムイベント・ウォッチャの変更・\<core-signals>要素といった選択肢があります。
これが私たちにできる最良の方法でしょうか? もちろん違います。 イベントのために、ファイア・アンド・フォーゲットを行えば、グローバルなイベントシステムモデルは比較的うまく動作しますが、一度ステートフルなイベントやチェインニングを必要とし始めれば、困難になってしまいます。 複雑性が増せば、イベントがコミュニケーションとフロー制御を織り交ぜていることに気付くでしょう。一方で、イベントシステムを改良するためのたくさんの方法があり(例えば関数型リアクティブプログラミングなど)、イベントが任意で大量のコードを実行していることが分かるでしょう。
グローバルなイベントシステムよりも優れているものは CSP です。 並行システムにおいてコミュニケーションを記述する、形式化された方法です。CSPのチャネルは、ClojureScriptやGoなどで見られ、 core.async プロジェクトで形式化されてきました。CSP(Content Security Policyと間違えないでください)は、メッセージパッシングを利用して、チャネルから消費したり置いたりする際の実行をブロックし、複雑な非同期フローを表現しやすくします。これらが解決する問題は、ある点では二重のチャネルを必要とし、下記のような従来の文字列型の方法に頼ります。
thingNeedsPhoto { id: 001, uuid: “foo” }
thingPhoto { data: “../photo.png”, uuid: “foo” }
後でこの二つを合致させます。これで不可逆の最適化されていないイベントチャネルで、関数呼び出しをほとんど再実装しました。タイポのせいでミスマッチが起こるといつも、デバッギングで苦労します。
CSPから引き出されたメカニズムは、他の動作を制御するための抽象化を提供します。このようなプリミティブを使えば、同時実行する協調的なメカニズムを構築することはもっと簡単です。 CSPは比較的低水準の構成のセットを与えてくれます。関数型リアクティブプログラミングでは、シグナルは時間とともに変化する値を示します。シグナルはプッシュ型のインターフェースを持っており、それで反応がいいのです。FRPは純粋な関数型インターフェースを提供するのでイベントが放出されることはありませんが、制御フロー構造によってベースシグナルの変換が定義できます。FRPのシグナルはCSPのチャネルを使って実装可能です。
James Long と Tom Ashworth は、CSPに関する確かな記事をいくつか投稿しています。グローバルなイベントシステム以上のものが欲しいと思っているなら、Transducers(合成可能なアルゴリズム変換)に注目してみるといいでしょう。
また、 js-csp プロジェクトをチェックしてみてください。これは、マクロよりむしろジェネレータ(yield文)を使って制御の反転(IOC)のカプセル化を実装した、ClojureScriptのcore.asyncに近いJavaScriptポートを提供します。
ES6およびBrowserify
以前、分離してJavaScript”モジュール”へ健全にアプローチする利点を生かした大規模なシステムの純益について書いたことがあります。私たちは数年前よりはるかにいい状況にいて、もはやAMDやRequireJS、モジュールのパターンで間に合わせるだけではありません。
信頼性の増した豊富なBrowserify関連ツール一式(Browserifyで使えるnpmモジュールの 1セット全部 )や、有り余るほどの トランスパイルに適した ES6機能に感謝しましょう。 ES6モジュール が最終的にブラウザに実装されるのを待つ間、それらはとても役に立ちます。
ソースマップをフルサポートした、ES5とES6で作業するための比較的良いパイプラインをもたらした @thlorenz と @domenic によるes6ify
ほとんどの場合、これは克服されていて、オーサリングワークフローのビルドステップを使う人に対して嫌な顔をしたのは、もはや過去になりつつあります。 JSライブラリの作者の 多く は、同じように喜んでES6を構築済みソースに使います。
ES6モジュールによって、私たちが依存とデプロイメントで直面してきた膨大な問題が解決され、私たちはexportsと明示してモジュールを作成し、名前のついたexportsをそのモジュールからインポートし、名前ごとに分けておくことができます。 ES6モジュールは、AMDの非同期の性質(ブラウザで必要)とCommonJSに見られるコードの明快さを組み合わせたようなもので、さらにより良い形で循環依存に対処します。
ES6モジュール内のdepsが静的なので、静的に分析可能な依存グラフがあり、これは非常に役に立ちます。また、CommonJSを使用するより(Browserifyワークフローのおかげでかなり快適に使用できるのにもかかわらず)すっきりしています。CommonJSをブラウザのコンテキストでサポートするために、モジュールをラップするかXMLHttpRequestに入れるかして、自分自身とevalをラップしてください。これでブラウザで動作できるようになりますが、ES6モジュールは最初からWebとサーバのユースケースをサポートするために設計されたので、この両方を見事にサポートします。
ネイティブの面では、ES6プリミティブ向けのサポートがV8やChakra、SpiderMonkey、JSCでネイティブに調査され続けるのを見て興奮もしました。恐らく私が最も驚いたのは、IE Technical Previewが ES6互換性表 で32/44まで急上昇して他を上回ったことでしょう。
IE Technical Previewは、既にES6のClass、for…of文、Map、Set、型付き配列、Array.prototypeメソッド、その他多くの機能をサポートしています。
私が投稿してきたV8の進歩に関する最新情報を見逃しているといけないので、念のためいくつか挙げておきます。
テンプレート文字列/リテラルの埋め込み表現で文字列を挿入しやすくなりました。
オブジェクトリテラルが拡張され、プロパティやメソッドを簡潔に記述できるようになってキーを打つ数が減りました。
ES6クラスによって現在のオブジェクトとプロトタイプに糖衣構文がもたらされました。
ES6の機能だろうとCommonJSモジュールだろうと、プロジェクトを強力に合成するのに十分なツール一式があります。クライアントでもサーバサイドでも、同形でもそうでなくても問題ありません。ちょっとした驚きです。誤解しないでほしいのですが、エコシステムの質を十分に向上させるには長い道のりが待っています。しかし現在、フロントエンドの合成は有力な話です。
一方、Webコンポーネントについて既に話しましたが、 HTML Imports に関してはここでも言及する価値があります。JavaScriptモジュールは常にコンポーネントとそれに対応するテンプレートに最も適した コンテナフォーマット であるとは限りません。多くの人々がコンポーネントをロードして解析するために追加で別のツールもまだ使っています。
JS開発者として私たちには既存のいくらか発達したスクリプト関連ツールのエコシステムがあるので、HTMLに戻ったり、依存のメカニズムとしてそれをサポートするためにツールを書き換えなければならなかったりするのは後退しているように感じられると言われることがあります。この問題は(importsを平たん化する) Vulcanize のようなツールで一部 解決されていて 、うまくいけばHTTP2では問題がなくなると思います。
純粋なスクリプトにするかインポートにするか検討すべき機械がどれくらいあるかや、ES6モジュールinteropを検討し始めた時にどこに線が引かれるかについて、私は個人的に葛藤しています。そうは言うものの、HTML importsは両方のパッケージコンポーネントリソースへのいい方法で、パーサをブロックせずにスクリプトを常にロードしています(それでもロードイベントはブロックします)。私はまだ、今後もimportsの利用や、モジュール、両方のシステムのinteropの発展が見られるという希望を抱いています。
PART2はこちら : 【翻訳】2015年に向けたJavaScriptアプリケーションアーキテクチャ PART 2
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa