SPAじゃないプロジェクトのための控えめなJavaScriptフレームワーク「Stimulus」

ReactやAngularのように、アプリ内のすべてのHTMLをJavaScript側で管理して描画するアプローチは大げさすぎる。ほとんどの場合において。

もちろん一定以上の複雑さがあればその辺のJavaScriptフレームワークを使った方が良い。が、それがやり過ぎになるプロジェクトは多い。というかその方が多い。それでも、それらのアプローチではやり過ぎになってしまう場合でも、秩序のあるコードを書き続けられないと当然苦しい状態へ向かう。

この微妙な(そして多数派であるはずの)環境を適度に管理できる枠組みが必要だ。これまでちょうどいい解決策を見つけられずに苦労してきた。Web Componentsは課題を解決するかもしれないがまだ来ない。そこで出会ったのがStimulusだ。

いわく、Stimulusは既存のHTMLのための控えめなJavaScriptフレームワーク。サーバーサイドレンダリングされたHTML(これはIsomorphicとかじゃなくて普通の)に対して、特定の属性が付与されていれば対応するコントローラを初期化する。

まずHTMLにはdata-controller属性、必要に応じてdata-targetdata-action属性を追加する。

<!-- HTML片は好きなところに -->
<div data-controller="hello">
  <input data-target="hello.name" type="text">

  <button data-action="click->hello#greet">
    Greet
  </button>

  <span data-target="hello.output">
  </span>
</div>

対応するコントローラを作成すると、Stimulusはその振る舞いを登録する。

// hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "name", "output" ]

  greet() {
    this.outputTarget.textContent =
      `Hello, ${this.nameTarget.value}!`
  }
}

これのよくできてるところは、単に読み込み時にだけコントローラの初期化が行われるわけでなく、ドキュメントへコントローラに対応するDOMが挿入されるたびに自動的に初期化される。削除されたら勝手に破棄される。さらにそのためのライフサイクルフックがある(initialize/connect/disconnect)

// 好きなタイミングでDOMを挿入すれば勝手に初期化される
document.body.innerHTML = `<div data-controller="hello">
  <input data-target="hello.name" type="text">

  <button data-action="click->hello#greet">
    Greet
  </button>

  <span data-target="hello.output">
  </span>
</div>`

// 好きなタイミングでDOMを削除すれば勝手に破棄される
document.body.innerHTML = ''

これは、StimulusはそもそもTurbolinksと協調するために作られたからだ。だからTurbolinksと組み合わせて使うこともできる。

これだけ書けば求める人にはグッと来るものがあるはずだ。詳しい使い方は公式ハンドブックのサンプルコードを見れば大体わかる。中途半端にまとめてもすぐに腐るので触らない(APIの変更が多いわけではないが)。思想的な序文だけ訳しておくことにする。

次からStimulusの起源(The Origin of Stimulus)の翻訳。


私たちはBasecampでたくさんのJavaScriptを書いているが、現代的な意味での「JavaScriptアプリケーション」は作成していない。すべてのアプリケーションはサーバーサイドレンダリングされたHTMLをベースにして、それらに付与する形でJavaScriptを追加している。

これが雄大なモノリスのやり方である。Basecampは、Ruby on Railsを使って作成されたコントローラ、ビュー、モデルの単一セットを持ち、ネイティブモバイルアプリを含むいくつかのプラットフォームで動作する。多くのプラットフォームがあるにも関わらず、小さなチームでやっていくためには、1つの共有インターフェイスを1箇所で更新できることが重要だ。

すると昔のように生産性の高いパーティーができる。1人のプログラマーが間接的なシステムや分散システムのレイヤーに縛られることなく、貪欲に進歩できる時代に逆戻りする。誰もにとっての究極の目標は、サーバーサイドアプリケーションがクライアントサイドアプリケーションのためにJSONを生成するだけのものになる前に戻ることだ。

そのようなアプローチに価値がないというわけではない。しかしそれは多くのアプリケーションにとって、そしてBasecampにとって、複雑性と生産性の低下をもたらす。

シングルページのJavaScriptアプリケーションにメリットはある。ページ全体を更新せずにより高速で流動的なインターフェースを実装できることだ。

Basecampもそのようだと感じて欲しかった。まるで流れに乗って、レンダリングをクライアントサイドで行うようにすべて書き直したか、完全なネイティブアプリになったかのように。

この要求が私たちをTurbolinksとStimulusという強力な組み合わせに導いた。

Turbolinkは高く、Stimulusは低く

私たちのこの新しい控えめなJavaScriptフレームワークであるStimulusを紹介する前に、これを使ってTurbolinksの命題を要約しておこう。

TurbolinksはGitHubで開発されたpjaxと呼ばれるアプローチから派生している。基本的な概念は同じままだ。

ページ全体のリフレッシュに時間がかかるのは、ブラウザがサーバーから送信される大量のHTMLを処理しなければならないからではない。それに関してブラウザは非常に優れており、非常に高速だ。そしてほとんどの場合、HTMLペイロードJSONペイロードよりも大きくなる傾向がある事実も問題にはならない。要因は、CSSJavaScriptを再初期化して再度ページに適用する必要があることだ。ファイル自体がキャッシュされているかどうかに関係なく。CSSJavaScriptのサイズによってはかなり遅くなる。

この再初期化を回避するためにTurbolinksは、シングルページアプリケーションと同じように永続的なプロセスを維持する。リンクのクリックイベントを乗っとり、Ajaxによって新しいページをロードする。サーバーは完全な形式のHTMLドキュメントを返す。

この戦略だけでも多くのアプリケーションを非常に速く感じさせられる。Basecampではページからページへの移動が約3倍速くなった。この高速化はアプリケーションに反応性とシングルページアプリケーションの魅力の大部分を占めていた流動性を与える。

しかしTurbolinksだけでは話の半分にしかならない。ページの読み込み以降、1つのページ内にはあらゆる振る舞いが登録される。要素の表示と非表示の切り替え、クリップボードへのコンテンツのコピー、リストへの新しいTodoの追加、および昨今のウェブアプリケーションに関連づけられているその他すべての操作を行うインタラクション。

Stimulus以前、Basecampはこれらをさまざまなスタイルやパターンを使って実装していた。コードの中にはjQueryを使ったちょっとしたものもあれば、同じようなサイズの単純なバニラJavaScriptもあり、また、より大きなオブジェクト指向のサブシステムであるものもあった。これらすべてはいつも、明示的なイベント処理によってdata-behavior属性を無効にしていた。

data-behaviorJavaScriptによる振る舞いをビューに依存せずに管理するための方法論。詳しくはCode Spelunking in the all new Basecamp – Signal v. Noiseを参照)

このような新しいコードを追加するのは簡単だったが、包括的な解決策ではなく、社内のスタイルやパターンが多すぎた。そのためコードの再利用が難しくなり、新しい開発者が一貫したアプローチを習得することが困難だった。

Stimulusの3つの核となるコンセプト

社内の良いパターンから抽出した次の3つが主要な概念となった。

  • コントローラ
  • アクション
  • ターゲット

Stimulusはこれらを控えめで小さなフレームワークとしてまとめたもの。

対応付けられたHTMLを見ると、Stimulusはプログレッシブエンハンスメントとして読み込まれるよう設計されていることがわかる。これにより、どの振る舞いが作用しているのかをテンプレートを見て理解できる。例を示そう。

<div data-controller="clipboard">
  PIN: <input data-target="clipboard.source" type="text" value="1234" readonly>
  <button data-action="clipboard#copy">Copy to Clipboard</button>
</div>

例を読むと何が起きているのかよくわかる。Stimulusについて何も知らなくても、あるいはコントローラのコードを見なくても。これは対応するイベントハンドラーが外部JavaScriptファイルに存在するHTML片を読むことと大きく異なる。また、現代の多くのJavaScriptフレームワークで失われている関心の分離も維持している。

ご覧のように、StimulusはわざわざHTMLを作成するのではなく既存のHTMLドキュメントに取り付けられる。ほとんどの場合、HTMLはページ読み込み時(最初の読み込みまたはTurbolinks経由)に、またはDOMに作用する起点になるAjaxリクエストによってサーバー上でレンダリングされる。

Stimulusはその既存のHTMLを操作することに関心がある。例えば、要素を非表示にしたりアニメーションさせたり、ハイライトさせたりするCSSクラスを追加すること。グループ内の要素を再配置させたりすること。UTCを地方時の日時表記に変換するように要素の中身を操作することなど。

Stimulusに新しいDOM要素を作成させたいこともあるが、それも自由に行える。将来的には簡単にできる方法を提供するかもしれない。しかしこれはごく少数のユースケースだ。焦点は要素の作成でなく操作にある。

Stimulusが主流のJavaScriptフレームワークとどのように違うのか

そのため、Stimulusは現代のJavaScriptフレームワークの大部分とはかなり異なっている。ほとんどのフレームワークは何らかのテンプレート言語を介してJSONをDOM要素に変換することに焦点を当てている。それらのフレームワークでは多くの場合、空のページを作成し、そうしてJSONからレンダリングした要素だけをそのページに追加する。

主流のJavaScriptフレームワークとStimulusでは状態の扱いも異なる。ほとんどのフレームワークには、JavaScriptオブジェクト内に保持した状態に基づいてHTMLをレンダリングする機能がある。Stimulusはその正反対だ。状態はHTMLに格納されるため、ページが変更されるたびにコントローラを破棄できる。そしてキャッシュされたHTMLから再び表示されてすぐの状態から再初期化できる。

これはまったく異なるパラダイムだ。現代のフレームワークに慣れているベテランのJavaScript開発者の多くはこれをあざ笑うに違いない。React+Reduxのような巨大な渦巻きの中で、アプリケーションを維持するために必要な複雑さと労力に満足しているなら、Turbolinks+Stimulusには魅力を感じないだろう。

一方で、現代の技術が示唆するような複雑さとアプリケーションの分離が不要だと感じている場合は、このアプローチに退避することになる。

Stimulusや関連するアイデアは現実世界から抽出された

Basecampではこのアーキテクチャをいくつかの異なるバージョンのBasecampや他のアプリケーションで長年使用してきた。GitHubも同様のアプローチを使って大きな効果を得ている。これは「現代」における、ウェブアプリケーションがどのようなものであるかの主流の理解に対する有効な代替手段である。それも非常に説得力のある。

実際、Stimulusは私たちがRuby on Railsを開発したときにBasecampで使った秘伝のタレに似ている。現代の主流のアプローチは不必要に複雑になっている感覚がある。私たちは本当はより多くのことをより速く、より少ないリソースで実現できるように思う。

あなたはそのいずれかだけを選択する必要はない。StimulusやTurbolinksはよりヘビーな他のアプローチと組み合わせて使うと効果的だ。もしあなたのアプリケーションの80%がヘビーな仕組みを必要としないのであれば、私たちのようにStimulus+Turbolinksを使うことを検討して欲しい。そしてアプリケーションの中の、実際に恩恵を受けられる部分でヘビーな仕組みを展開すると良い。

Basecampでは必要に応じていくつかの強力なアプローチを採用している。例えば、私たちのカレンダーはクライアント側のレンダリングを使用する傾向にある。私たちのテキストエディタTrixという完全な形のテキストプロセッサで、Stimulusコントローラの位置付けとしては意味を成さない。

一連の代替フレームワークはできるだけ重労働を回避することを目的としている。この単純なモデルでうまく機能する多くのインタラクションに対して、リクエスト・レスポンスパラダイムの範囲内に留まる。そして必要性が真に迫っていれば高価なツールに手を伸ばす。

何よりもこれは、より労力がかかる主流のアプローチを使う大きなチームと、忠実さで競争したいと思っている小さなチームのためのツールキットである。

試してみてごらん。

デイヴィッド・ハインマイアー・ハンソン