Stimulus、悪くない

Stimulus: A modest JavaScript framework for the HTML you already have.

2020年末にリリースされた Stimulus 2.0 を試して気づいたことをメモしておく。Stimulus は Rails で有名な Basecamp のメンバーによって開発された生 DOM をベースに、できるだけ template に JS を書かずに動的な DOM の生成、変更を実現するためのライブラリである。

昨今流行りの React, Vue, Angular などの VirtualDOM 系のツールとは根本的に考え方が異なり、今からこれを採用するのはやや申し訳ないというか、正直に言うとよいイメージはなかったし、例えばフロントエンドエンジニアとしてスキルアップしたいぜーという人には合わないと思うが、ただ、どうせサーバサイドの開発が必要で、分業できるほどパワーに余裕のない現場においては VirtualDOM 系よりもかなり高い生産性を維持できると言えそうだ。

少なくとも多能工と多能工が組んでアジリティを確保していこうぜ、という2000年代の空気、Rails誕生前後の空気を再び感じることができる。実際に少ないコードでそこそこのものを高速に開発してオレツエー感を味わえるという意味ではかなりよい選択肢の一つと言えると思う。

StimulusはDOM操作の世界でのよいレール

Stimulusのイメージコード

<div data-controller="counter">
  <span data-counter-target="count"></span>
  <button data-action="click->counter#up">up !</button>
</div>
class CounterController extends Controller {
  static targets = ['count']
  static values = ['count']

  up () {
    this.countValue++
  }

  countValueChagend () {
    this.countTarget.innerText = countValue
  }
}

const app = Application.start()
app.register('counter', CounterController)

簡単に説明すると

  • data-controller で指定した DOM 要素と Controller インスタンスが紐づく1
  • Controller の中で変化を検知できる値として values があり、DOM 側からは data-*-value=<identifier> でアクセスできる
  • Controller 側から操作可能な DOM 要素として targets がある2
  • Controller のメソッドを呼ぶには data-action を利用する
    • event は default のものは省略することができ、この書き方を利用すると event を意識することなくメソッドを呼び出しているように見える

伝統的なDOM操作の世界でのStimulusのメリット

  • DOM の影響範囲を data-* で限定し、そこに効力を発揮する JavaScript を Controller と呼び、関係する処理をこの中に閉じ込めることができる
    • 要素が複数あった場合は自動的に別々の Controller インスタンスと bind されるのでお互いに干渉しない
  • event driven と reactive への習熟という、伝統的なプログラミングスタイルからの飛躍を必要としない
  • DOMContentLoaded 周りの繊細さがない
  • CDN から読み込んだだけで動作する(ES2015 と Proxy に対応しているブラウザが必要)
  • template 側に JS を一切書かずに済むので template 側は副作用込みの行儀悪いものにならない

伝統的なDOM操作コードの問題点とStimulusのアプローチ

残念ながらこの日記にはまとめていないんだけど、上のメリットについては自分も以前からとても頭を悩ませていた。

初歩的な jQuery や VanillaJS で書いたちょっとした装飾や効果、遅延読み込みを実現するコードはほぼ必ずと言っていいほど以下のような問題を抱えている。

  • (event が起きてから DOM の走査を行うので)どこまで影響が広がっているのか想像がつかない
  • やりたいことに名前がついていないので意図が分からない

これは本当に致命的な問題だと思っていた。3

この問題を解消するには 対象と操作を明示する名前を付けたコードの中で、明示した範囲内の DOM だけを操作対象にする しかないと考えていて、自分でそういうツールを書いたりもしていたんだけど4、Stimulus はほぼそのままの回答になっている。

VirtualDOM系ツールを使わないメリット

VirtualDOM系ツールを利用した開発時の課題

VirtualDOM と reactive という考え方は

  • もろもろの「操作」系のコードを省くことでおかしな副作用が途中で混ざり込んでしまう可能性を排除できる
  • event を stream として functional に処理できる

など、より複雑な処理により安全に対応するよい作法である。

しかし逆に、VirtualDOM 系のツールには以下のような問題がある。

  • サーバサイドで組み立てていた HTML の世界に一切歩み寄りがなく、サーバサイドを書いていた手を止めてクライアンドサイドの考え方とツール群に完全に持ち替えないとコードが書けない。完全に分業できる組織ならそれでもよいかもしれないが、特にプロダクトの初期実装時にとても扱いにくく感じる。
  • うまく作用する component を作ろうと思うととても小さいものになり、結果として DOM ツリー全体を構築するためには当初の想像の何倍もの数の component を作らなきゃいけなくなりがち
  • 数多くの component を書いているうちにすべて component だけで解決しなきゃいけないような気になる → 誤った設計を誘発しがち

簡単に言うと VDOM 系は基本的に高くつく。設計ミスも誘発する。そこまでしても速くなるのは DOM の書き換えだけであり、最初の表示は逆に遅くなったりするので、意外と最適化しないと体感速度は改善しない。

VirtualDOM系ツールに対するStimulusのメリット

Stimulus はこれらの問題のすべてを回避できる。改めてまとめ直すと、以下のようにかなり低コストに動的な DOM を利用した体験を実現できる。

  • VDOM 系のツールを動作させるセットアップが一切不要
  • サーバサイドとクライアントサイドの両方をまたぐ処理を書く際に起きる分断がほとんどなく、概ねサーバサイドのコードのような書き味で作れる
  • Stimulus の Controller は VDOM 系の component より少し大きな粒度になるので数をかなり減らせる

もっと細かく調整したり様々なイベントを扱いたいといったニーズはあると思うが、

ツールの大きな持ち替えで手が止まることなくだいたいのケースに対応できる

という実に Rails っぽい考え方でうまくまとまっていると思う。

もちろん、より複雑なロジックやグローバルなステートなどは Controller の守備範囲外だ。その辺りは当然の前提としたうえで考えると、実にニーズナブルでよい解決方法に見える。

そして、実はここが大きいんだけど

  • template 側でのデザインと Controller 側での処理の分業も行いやすい

これ。

VDOM 系のツールを使うと必要以上にデザイナが VDOM 系のツールへの習熟を必要としてしまい、例えばデザイナがツールの習熟度で詰まった際にバックエンドの処理を書いているエンジニアにヘルプがくるとそこでエンジニアの手が止まってしまう。

つまり、

VDOM 系のツールは実はフロントエンドにある程度余裕のあるエンジニアを確保できていないと、デザインの実装に対してもバックエンドの実装に対してもブレーキになり得る

という問題を抱えている。これを解消できる Stimulus というアプローチがとても助かる現場は多いと思う。

Stimulusのデメリット

もちろんデメリットもある。

  • テストしたければ結局フロントエンドのツールチェインの整備は必要
    • そりゃそうだ。もしかしたら頑張って Selenium 経由で console のエラーを拾う、みたいなこともできなくもないかもしれないけど、それはたぶんかなりつらいと思う。
  • Controller のユニットテストは難しい
    • 生 DOM と紐づいてこそ意味があるので、独立したテストは意味を成しにくい。純粋な関数部分のテストはできなくもないが、そもそも Controller に複雑な処理を入れるな、でよいと思う5
    • テストしたければ小さなテスト用の DOM と一緒にセットアップするコードを作る

総じて テストコードを書くモチベーションが下がる と思う。これは凝ったことをし始める時、量が多くなった時に突然牙を剥く可能性がある。

※ もっとも、生 DOM を使ったテストというものはセットアップさえ分かってしまえば書くこと自体はそんなに難しくはない。少なくとも Turbo のように DOM 外の resource へのアクセスが絡まないうちは。(Stimulus の場合は DOM を Controller が mount する部分で工夫が要るけど)

あとこれはデメリットじゃないけど、

VDOM + State Management の開発と異なり、生DOM相手なのでデバッグもブラウザの素の Developer Tools を利用する

ことになる。

参考

  1. DOM 側からは register した名前で紐づける 

  2. どのように操作するかは決まっていない。上の例のように直接書き換えてもいいし、何らかの JavaScript の template を挟んでそこでコンテンツを生成してもよい 

  3. 少なくとも jQuery をやめれば解決するという話ではない。jQuery 的な神オブジェクトを採用するすべての SDK がこの問題を抱えていると言ってよい。 

  4. しかも偶然にも class ベースで書かれている 

  5. 恐らくだが Controller へのアクセス方法を DOM の data-* だけに限定できていて「意図しない呼び出しが行われていない」ことを前提にした設計になっているので、そもそもカジュアルにインスタンスを生成して動かせないようになっていると予想される。 

More