12.2K Views
August 24, 24
スライド概要
今日はRubyKaigi公式スケジュールアプリをReactのSmartHR UIからHotwireに置き換えたので、実際にRubyKaigiで運用してどんな問題に直面したのかを話そうかと思います
RubyKaigi公式スケジュール アプリで得たHotwireの知見 2024.08.24 Sat. 大阪Ruby会議04@フェスティバルタワー中之島会館 kinoppyd SmartHR プログラマ
Hello 大阪
ワレなんぼのもんや kinoppyd Programmer at RubyKaigi公式スケジュールアプリメンテナ 酒コードポーカーロードバイク料理
今日はRubyKaigi公式スケジュールアプリを ReactのSmartHR UIからHotwireに置き換えたので 実際にRubyKaigiで運用してどんな問題に直面したの かを話そうかと思います
おさらいHotwire
Hotwire? • Turbo(話す) • Stimulus(ほんのわずかに話す) • Strada(話さない)
Turbo? • Turbo Drive - ほぼTurbolinks • Turbo Frames - ページ部分書き換え • Turbo Streams - 一言で言えない
TurboFrames
<html> <body> <h1>hoge</h1> <turbo-frame id="a"> <h2>foo</h2> <a href="/edit">click</a> </turbo-frame> <div> something </div> </body> </html> hoge foo click something
<html> <html> <body> <body> <h1>hoge</h1> <h1>hoge</h1> <turbo-frame id="a"> <turbo-frame id="a"> <h2>foo</h2> <h2>piyo</h2> <a href="/edit">click</a> <a href="/edit">click</a> </turbo-frame> </turbo-frame> <div> <div> something something </div> </div> </body> </body> </html> </html> hoge foo click something
<html> <body> <h1>hoge</h1> <turbo-frame id="a"> <turbo-frame id="a"> <h2>foo</h2> <h2>piyo</h2> <a href="/edit">click</a> <a href="/edit">click</a> </turbo-frame> </turbo-frame> <div> something </div> </body> </html> hoge foo click something
<html> <body> <h1>hoge</h1> <turbo-frame id="a"> <turbo-frame id="a"> <h2>foo</h2> <h2>piyo</h2> <a href="/edit">click</a> <a href="/edit">click</a> </turbo-frame> </turbo-frame> <div> something </div> </body> </html> hoge foo click something
<html> <body> <h1>hoge</h1> <turbo-frame id="a"> <h2>foo</h2> <h2>piyo</h2> <a href="/edit">click</a> </turbo-frame> <div> something </div> </body> </html> hoge foo piyo click something
TurboStreams
<html> hoge <body> <h1>hoge</h1> <turbo-frame id="a"> <h2>foo</h2> <a href="/edit">click</a> foo click </turbo-frame> <div id="b"> something </div> </body> </html> something
hoge <html> <body> <turbo-stream <h1>hoge</h1> action="prepend" target="a"> <turbo-frame id="a"> <template> foo <h2>foo</h2> <h2>piyo</h2> click </template> <a href="/edit">click</a> </turbo-frame> </turbo-stream> something <turbo-stream <div id="b"> action="replace" target="b"> something <template> </div> anything </body> </template> </html> </turbo-stream>
<turbo-stream <html> action="prepend" target="a"> hoge <body> <template> <h1>hoge</h1> <h2>piyo</h2> </template> <turbo-frame id="a"> <h2>foo</h2> </turbo-stream> <a href="/edit">click</a> foo click </turbo-frame> <div id="b"> something </div> </body> </html> <turbo-stream something action="replace" target="b"> <template> anything </template> </turbo-stream>
<html> hoge <body> <h1>hoge</h1> <turbo-frame id="a"> piyo <h2>piyo</h2> foo <h2>foo</h2> <a href="/edit">click</a> click </turbo-frame> <turbo-stream <div id="b"> something action="replace" target="b"> something </div> <template> </body> anything </html> </template> </turbo-stream>
<html> <body> <h1>hoge</h1> <turbo-frame id="a"> <h2>piyo</h2> <h2>foo</h2> <a href="/edit">click</a> </turbo-frame> <div id="b"> anything </div> </body> </html> hoge piyo foo click anything
Stimulus
Stimulus? • JavaScriptの書き方フレームワーク • Controller(not Rails)をDOMにバインドする • DOMで閉じたコンポーネントを作るイメージ
React -> Hotwire
React -> Hotwire • SmartHR UIのリリーススピードは速い • HTMLのセマンティクスを尊重したい • RubyKaigiだしRailsのデフォ使いたい
HTMLのセマンティクス • Turboは、HTMLでRailsと会話する • Railsの愚直なREST/CRUD画面を再利用する • htmxなどJSでも似た思想のライブラリがある
Railsの愚直なREST/CRUD • クライアント側でステートを制御しない • サーバー側で描写するHTMLがすべて • 今までのRailsのメンタルモデルでSPA likeな 画面を作れる
Move to Hotwire
直面した課題の一部
直面した課題の一部 • tableタグの更新難易度が高い • Dialogとの組み合わせがムズい • エラーの出し分けが大変
タブUI • 分割したほうがいいのか……? • しかしタブはアンカーである • HTML的には一度に読みたい、分割せず、重い…… こいつ
テーブルのTurboFramesは制限が多い • tableやtrタグの中で使えるタグに強い制限がある • つまり行レベルのturbo-frameタグは置くことができない • このルールを破るには、TurboStreamで直接DOMを操作 するしかない これ https://developer.mozilla.org/ja/docs/Web/HTML/Element/table
関連するボタン • ただですらテーブルのTurboStreamsはムズい • <tr> レベルでパーシャル化して対応 • 課題:Stimulusをつかってリクエスト中押せぬように こいつら
formタグの中のaタグやボタン • formタグの中のaはhref="#"でもTurboが発火する • formタグの中のbuttonはデフォルトでsubmit、発火する • 余計に通信が発生するのでbutton type="button"を使う やつ
モーダルも難しい • dialogタグのお陰で表示はだいぶ楽 • 制御が難しい、エラーハンドリングが複雑 • 成功時は画面の、失敗時はモーダルのFrameが必要 こんなの
DHHはモーダルをどう表示してる? • DHHが作ったCamp reというプロダクトがある • 有償だが、コードの参照が可能 • モーダルは、モーダルっぽい動きをする何かであり、 fi 完全に独立したURLをもった一つのページ
DHHはモーダルをどう表示してる? • モーダルはHotwireでなるべく使わないほうがいい • すべての画面には専用のURLを用意するべき • 必要なら、どう見せるかで工夫するほうが良し
Hotwire in real world
Hotwire in real world • フロントは通信速度に大きく左右される • 特にHotwireはその傾向が顕著に現れる • どうやって回避するか?
TurboFramesは装飾をしない
TurboFramesは装飾をしない 同時にクリックをして、画面の遷移を比較
TurboFramesは装飾をしない • 左パターンはクリック後しばらく固まる • 右パターンはまず空のダイアログが出てくる • ユーザーが困らないのは右パターン
TurboFramesは装飾をしない
TurboFramesは装飾をしない
<a
href="<%= schedule̲dialog̲url(schedule) %>"
data-turbo-frame="modal"
>
<turbo-frame id="modal"></turbo-frame>
<div data-controller="dialog">
<img
data-action="click->dialog#open"
src="<%= image̲url trophy.icon̲url %>"
/>
<dialog id="<%= trophy.id %>-dialog" class="dialog">
<%= turbo̲frame̲tag trophy, src: trophy̲path(trophy),
loading: :lazy %>
</dialog>
</div>
TurboFramesは装飾をしない
aタグでTurboFramesを呼び出す。
戻ってきたHTMLの中にDialogタグが書か
れているそのため、クリック直後に表示す
るDOMが存在しない。
<a
href="<%= schedule̲dialog̲url(schedule) %>"
data-turbo-frame="modal"
>
<turbo-frame id="modal"></turbo-frame>
<div data-controller="dialog">
<img
data-action="click->dialog#open"
src="<%= image̲url trophy.icon̲url %>"
/>
<dialog id="<%= trophy.id %>-dialog" class="dialog">
<%= turbo̲frame̲tag trophy, src: trophy̲path(trophy),
loading: :lazy %>
</dialog>
</div>
TurboFramesは装飾をしない
StimulusでdialogのDOMをあら
aタグでTurboFramesを呼び出す。
かじめ表示しておき、その後lazy
戻ってきたHTMLの中にDialogタグが書か
付きのTurboFrameでダイアログ
れているそのため、クリック直後に表示す
の中身のDOMを取得してくる
るDOMが存在しない。
<a
href="<%= schedule̲dialog̲url(schedule) %>"
data-turbo-frame="modal"
>
<turbo-frame id="modal"></turbo-frame>
<div data-controller="dialog">
<img
data-action="click->dialog#open"
src="<%= image̲url trophy.icon̲url %>"
/>
<dialog id="<%= trophy.id %>-dialog" class="dialog">
<%= turbo̲frame̲tag trophy, src: trophy̲path(trophy),
loading: :lazy %>
</dialog>
</div>
Eager/Lazy-loading? • TurboFramesを使った分割遅延読み込み • ページを構成する要素を、違うエンドポイン トから取得してturbo-frameタグの中にいれ ることができる
Eager/Lazy-loading? https://turbo.hotwired.dev/handbook/frames
TurboFramesは装飾をしない • 事前に表示できるDOMは表示する • 大きなパーツをTurboFrames化しない • TurboFramesのEager/Lazy-loadingを使っ て代替コンテンツを表示する
CacheとPreviewはあまり うれしくない
CacheとPreviewはあまりうれしくない • TurboDriveにキャッシュという仕組みがある • DOMを覚えておき、再訪問した時に表示する • 戻る/進むはCache、遷移はPreview
CacheとPreviewはあまりうれしくない ダイアログを閉じてから画面遷移し、戻るボタンを押すと……
CacheとPreviewはあまりうれしくない • StimulusとかでDOMをいじっていると意図 しない状態が残ってる • データ更新があった場合に正しくない画面を 描写している • 基本的にあまりうれしくない
CacheとPreviewはあまりうれしくない 完全にStaticでもない限りは切っておこう <head> <meta name="turbo-cache-control" content="no-cache"> </head>
代替コンテンツを表示しよう
代替コンテンツを表示しよう • Eager/Lazy-loadingは、通信中に代替コンテ ンツを表示できる • <turbo-frame>の中に書かれているDOM • 適切に設定して、ユーザーに読み込みを通知 する
代替コンテンツを表示しよう • スケルトンスクリーンが良い • レンダリングする予定のDOMをフレームだけ 表示する奴 • Cacheを切ったので、ユーザーに読込中を提 示するために必要になる
代替コンテンツを表示しよう 同時にページをリロードして、挙動を比較
代替コンテンツを表示しよう • 左パターンは読込中に空白の画面がでる • 右パターンは読み込みアニメーションが出る • 右はスケルトンスクリーン
代替コンテンツを表示しよう
代替コンテンツを表示しよう <% if turbo̲frame̲request? %> <%= turbo̲frame̲tag @event do %> <%= turbo̲frame̲tag @event do %> <%= render partial: "table" %> <% end %> <%= render partial: "table" %> <% end %> <% else %> <%= turbo̲frame̲tag @event, src: schedules̲path do %> <%= render partial: "table̲skeleton" %> <% end %> <% end %>
代替コンテンツを表示しよう <% if turbo̲frame̲request? %> <%= turbo̲frame̲tag @event do %> <%= render partial: "table" %> <% end %> <% else %> <%= turbo̲frame̲tag @event, src: schedules̲path do %> <%= render partial: "table̲skeleton" %> <% end %> <% end %>
代替コンテンツを表示しよう <% if turbo̲frame̲request? %> <%= turbo̲frame̲tag @event do %> <%= render partial: "table" %> <% end %> <% else %> <%= turbo̲frame̲tag @event, src: schedules̲path do %> <%= render partial: "table̲skeleton" %> <% end %> <% end %> Turboのリクエスト判定
代替コンテンツを表示しよう <% if turbo̲frame̲request? %> Turboのリクエスト判定 <%= turbo̲frame̲tag @event do %> <%= render partial: "table" %> <% end %> <% else %> <%= turbo̲frame̲tag @event, src: schedules̲path do %> <%= render partial: "table̲skeleton" %> <% end %> <% end %> 1. 自身をsrcにする
代替コンテンツを表示しよう <% if turbo̲frame̲request? %> Turboのリクエスト判定 <%= turbo̲frame̲tag @event do %> <%= render partial: "table" %> <% end %> <% else %> <%= turbo̲frame̲tag @event, src: schedules̲path do %> 1. 自身をsrcにする <%= render partial: "table̲skeleton" %> 2. スケルトンをレンダリング <% end %> <% end %>
代替コンテンツを表示しよう <% if turbo̲frame̲request? %> Turboのリクエスト判定 <%= turbo̲frame̲tag @event do %> 3. Turboならsrcなし <%= render partial: "table" %> <% end %> <% else %> <%= turbo̲frame̲tag @event, src: schedules̲path do %> 1. 自身をsrcにする <%= render partial: "table̲skeleton" %> 2. スケルトンをレンダリング <% end %> <% end %>
代替コンテンツを表示しよう <% if turbo̲frame̲request? %> Turboのリクエスト判定 <%= turbo̲frame̲tag @event do %> 3. Turboならsrcなし <%= render partial: "table" %> 4. テーブルをレンダリング <% end %> <% else %> <%= turbo̲frame̲tag @event, src: schedules̲path do %> 1. 自身をsrcにする <%= render partial: "table̲skeleton" %> 2. スケルトンをレンダリング <% end %> <% end %>
代替コンテンツを表示しよう • Eager-loadingの代替コンテンツを活用 • turbo-framesタグの中のDOMがまず描写さ れる • srcの読み込みが終わったら置き換えられる
代替コンテンツを表示しよう • Eager-loadingを使って自身を呼び出す • View内部で、Turboリクエストかどうかを判定する • 通常であればスケルトンを、Turboであれば実際のコン テンツを返す
代替コンテンツを表示しよう • まずスケルトンを返すと、レスポンスが速い • キャッシュをオフにした時間を取り返せる • 重いコンテンツはガンガンEager-loading!
代替コンテンツを表示しよう 🙌
まとめ
まとめ • TurboはRESTのメンタルモデルを保っている • 一方で、読み込みのフィードバックは壊れる • ユーザーに何が起こっているかをイベントや 代替コンテンツで通知しよう
まとめ Turboのイベント一覧リファレンスとかを見ると知見があります https://turbo.hotwired.dev/reference/events
まとめ • レイテンシが速い場所にデータ置こう • HerokuのUSは日本から遠い • グローバル展開するときは大事だね
まとめ • 来年はデザインの刷新を予定しています • よりモバイルで使いやすいものにします • また来年、松山でお会いしましょう