トップ 最新 追記

2021-02-23 [長年日記]

_ 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
    • テストしたければ小さな e2e のテストに

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

※ もっとも、小さな e2e のテストというものはセットアップさえ分かってしまえば書くこと自体はそんなに難しくはない。少なくとも Turbo のように DOM 外の resource へのアクセスが絡まないうちは。

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

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

ことになる。

参考

Tags: JavaScript

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

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

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

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

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


2021-02-25 [長年日記]

_ turbolinksで実現したかったことって、こういうことだったのか

Turbo: The speed of a single-page web application without having to write any JavaScript.

Stimulus ほどあれこれ考察はしていないけど、Turbo を軽く触ってみて結構よさげという印象を受けた。Google Analytics などの 3rd party のツールとの組み合わせの部分には注意が必要になるが、特に初期実装ではかなりの威力になりそうだ。

※ 今回は Turbo Stream は試していない。あくまで Turbo Drive と Turbo Frame の話。

Stimulusはturbolinksに欠けていたもの

まず あーありがち - Stimulus、悪くない の方を読んでほしいんだけど、Turbo は turbolinks が洗練されて戻ってきたものであることから始めると導入しやすいと思う。

細かく引用はしないが、turbolinks 登場時、最も困ったのは当時全盛と言ってよい jQuery を利用したコードと極めて相性が悪かった点であった。なんなら Rails 標準の JavaScript のライブラリが jQuery なのにどうなってんだこれは、という話である。

どういうことかというと、turbolinks を有効にしてしまうと jQuery を利用した以下のような典型的なコード

jQuery(function() {
  ..
})

が、初回のページロード時には動くが、ページ遷移やブラウザの「戻る」ボタンを押した際には動作しない、という問題があったのだ。というか今もこれはある。jQuery をやめて

document.AddEventListener('DOMContentLoaded', () => {
  ..
})

にしても問題は解決しない。なにせ turblinks も Turbo の Turbo Drive も

ページ遷移自体はせず、DOMの一部とHistoryへの変更であたかも遷移したフリをする

ものだからだ。

turbolinks や Turbo のために監視するイベントを増やしたりする方法もあるかもしれないが、それなら今なら潔く VDOM 系のツールで SPA を組む方が賛同を得られやすいだろう。ただし開発コストは大きく膨らむが。

しかし、Stimulus を使うことでこれは解消される。Stimulus ではこの部分は Application.start() の中に隠蔽されていて、DOM がゼロベースで構築完了したかバックグラウンドで必要な HTML を取得して一部の書き換えを行ったかに関わらず Controller の connect() メソッドは呼ばれる。

つまり、Stimulus を使うことでページの読み込みを監視する処理をそもそも自分で書く必要がなくなり、それによって Turbo を利用しているかいないかに関わらず安定して同じように動作するのだ。

これは非常に助かる。これまで turbolinks を off にするのは割とよくある話だったと思うが、フロントエンドエンジニアを多く確保しなくても Turbo と Stimulus を組み合わせることで、よくある多くのパターンの動的な DOM 構築に対応でき、明らかに体感速度は向上する。

実際にブラウザの Developer Tools で見てみると HTML を取得するたった一つの connection で、JavaScript も Stylesheet も読み込み直すことなくシュッと遷移するのは実に快感だ。

方法は簡単。例えば Webpack や Vite などの bundle ツールを利用していたら import するだけである。それだけで有効になり、ページ遷移は爆速になる。具体的には

import * as Turbo from '@hotwired/turbo'

かな。これだけでよい。使っていなくても大丈夫。CDN から script タグで読み込むだけで動く。

Turbo Frameはiframeを使わないiframeのような何か

これも非常によくあると思うのだが、HTMLの中の一部のコンテンツを遅延ロードしたいという場合、以下のように書くと実現できる。

読み込む側 /importer

<turbo-frame id="export" src="/exporter">
</turbo-frame>

読み込まれる側 /exporter

<turbo-frame id="export">
  ..
</turbo-frame>

これで importer 側の <turbo-frame> の中身が exporter 側の <turbo-frame> の中身に挿し変わる。

id でコンテンツを紐付けて対応する DOM を入れ替えてしまうという方法である。

同じことを JavaScript で実現する側として正解を挙げると、従来なら

  • サーバサイドで JSON を出力
  • クライアントサイドで JSON を取得
  • クライアントサイドで(サーバサイドとは異なる記法で)DOM を構築

という3ステップを要する。

しかし Turbo Frame を利用すれば全部端折ってサーバサイドで普通に HTML を書いて別な entry point を与えればよい。bot 避けに meta でも書いておけば変なお漏らしもないだろう。これだけで済む。

もちろん、そもそもコンテンツをどのように見せるかは import する側の責務なのでは? という指摘はあると思う。自分もそう思う。でもじゃあそのためにフロントエンドのツールでゼロから書きますか、というと「なんだかなぁ」という気がしないだろうか。DOM の書き換えだってせいぜい最初の一回しか起きないのに VirtualDOM とか必要ですか?という気持ちになるし、今書いているバックエンドのコードから完全に頭を切り変えてフロントエンドの海に潜らないといけない。

Turbo を使うとそこはずっとサーバというか HTML のままで実現できるので、スイッチングコストが発生しない。しかも Turbo Drive でのページ遷移と組み合わせると、遅延読み込みしたかどうかも人間にはよく分からない。

まとめ - Stimulus + Turboはよいぞ

以上のような形で

  • サーバ側の routing (あるいは単なる HTML のパス)で
  • サーバ側の HTML の生成で
  • JavaScript をいちいち書かずに

ユーザーの体験を大きく向上させるのが Turbo の狙いだ。

そしてより細かい部分や 3rd party のツールと組み合わせる際には Turbo 固有の event を利用すればよい。

Hotwire という名前や DHH という名前で大袈裟に構えることはない。なんてことのない、だが確実によく利く勘所を押さえたツールと言えそうだ。

フロントエンドのツールの理解、VirtualDOM や reactive という考え方はそれはそれで必要になるし、例えばタイマーのようなより細粒度、高頻度の更新が必要なものは JavaScript で閉じた方がよいと思うが、ほとんどサーバ側が正解の情報(HTMLなど)を持っているのに JavaScript から遠回りして DOM に反映しているケースも実は多い。

そういう場合に .js.erb のように無理やり JavaScript に情報を渡すのではなく、もうそのまま HTML を渡せばいいじゃんという考え方は一見大胆だし、細かくベンチマークを測るとそこまで速くもないかもしれないけど、そこそこ十分に速くて開発コストが大きく抑えられるのもまた事実。混ぜて使う際にはやや注意が必要かもしれないが、少なくとも初手でコストを掛けずにユーザーの体験をよくするためには大きく貢献してくれそうだ。

※ テストが難しそうというかエラーハンドリング周りの情報が全然ない*1状態なのは確かに気になるので、そこは追試が必要。あと HTML を「直接」「普通に」取得するのでサーバサイドや昨今のフロントエンドを書く際に忘れていたキャッシュ周りに気をつけないとハマる可能性もある。あと地味にレビューが難しい。Stimulus も Turbo も生々しく自分で実装する量がグッと減るので、テストコードも恐らくかなり少なくなる代わりに、HTML 側の記述ミス防止は恐らく目視チェックになりやすいので、何か工夫があった方がよさそう。

Tags: JavaScript

*1 普通に console にエラーが出るんだけど、その情報が十分に出揃ってない。