RubyKaigi公式スケジュールアプリで得たHotwireの知見

12.2K Views

August 24, 24

スライド概要

今日はRubyKaigi公式スケジュールアプリをReactのSmartHR UIからHotwireに置き換えたので、実際にRubyKaigiで運用してどんな問題に直面したのかを話そうかと思います

profile-image

どういうわけか暑がり

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

RubyKaigi公式スケジュール アプリで得たHotwireの知見 2024.08.24 Sat. 大阪Ruby会議04@フェスティバルタワー中之島会館 kinoppyd SmartHR プログラマ

2.

Hello 大阪

3.

ワレなんぼのもんや kinoppyd Programmer at RubyKaigi公式スケジュールアプリメンテナ 酒コードポーカーロードバイク料理

4.

今日はRubyKaigi公式スケジュールアプリを ReactのSmartHR UIからHotwireに置き換えたので 実際にRubyKaigiで運用してどんな問題に直面したの かを話そうかと思います

5.

おさらいHotwire

6.

Hotwire? • Turbo(話す) • Stimulus(ほんのわずかに話す) • Strada(話さない)

7.

Turbo? • Turbo Drive - ほぼTurbolinks • Turbo Frames - ページ部分書き換え • Turbo Streams - 一言で言えない

8.

TurboFrames

9.

<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

10.

<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

11.

<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

12.

<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

13.

<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

14.

TurboStreams

15.

<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

16.

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>

17.

<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>

18.

<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>

19.

<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

20.

Stimulus

21.

Stimulus? • JavaScriptの書き方フレームワーク • Controller(not Rails)をDOMにバインドする • DOMで閉じたコンポーネントを作るイメージ

22.

React -> Hotwire

23.

React -> Hotwire • SmartHR UIのリリーススピードは速い • HTMLのセマンティクスを尊重したい • RubyKaigiだしRailsのデフォ使いたい

24.

HTMLのセマンティクス • Turboは、HTMLでRailsと会話する • Railsの愚直なREST/CRUD画面を再利用する • htmxなどJSでも似た思想のライブラリがある

25.

Railsの愚直なREST/CRUD • クライアント側でステートを制御しない • サーバー側で描写するHTMLがすべて • 今までのRailsのメンタルモデルでSPA likeな 画面を作れる

26.

Move to Hotwire

27.

直面した課題の一部

28.

直面した課題の一部 • tableタグの更新難易度が高い • Dialogとの組み合わせがムズい • エラーの出し分けが大変

29.

タブUI • 分割したほうがいいのか……? • しかしタブはアンカーである • HTML的には一度に読みたい、分割せず、重い…… こいつ

30.

テーブルのTurboFramesは制限が多い • tableやtrタグの中で使えるタグに強い制限がある • つまり行レベルのturbo-frameタグは置くことができない • このルールを破るには、TurboStreamで直接DOMを操作 するしかない これ https://developer.mozilla.org/ja/docs/Web/HTML/Element/table

31.

関連するボタン • ただですらテーブルのTurboStreamsはムズい • <tr> レベルでパーシャル化して対応 • 課題:Stimulusをつかってリクエスト中押せぬように こいつら

32.

formタグの中のaタグやボタン • formタグの中のaはhref="#"でもTurboが発火する • formタグの中のbuttonはデフォルトでsubmit、発火する • 余計に通信が発生するのでbutton type="button"を使う やつ

33.

モーダルも難しい • dialogタグのお陰で表示はだいぶ楽 • 制御が難しい、エラーハンドリングが複雑 • 成功時は画面の、失敗時はモーダルのFrameが必要 こんなの

34.

DHHはモーダルをどう表示してる? • DHHが作ったCamp reというプロダクトがある • 有償だが、コードの参照が可能 • モーダルは、モーダルっぽい動きをする何かであり、 fi 完全に独立したURLをもった一つのページ

35.

DHHはモーダルをどう表示してる? • モーダルはHotwireでなるべく使わないほうがいい • すべての画面には専用のURLを用意するべき • 必要なら、どう見せるかで工夫するほうが良し

36.

Hotwire in real world

37.

Hotwire in real world • フロントは通信速度に大きく左右される • 特にHotwireはその傾向が顕著に現れる • どうやって回避するか?

38.

TurboFramesは装飾をしない

39.

TurboFramesは装飾をしない 同時にクリックをして、画面の遷移を比較

40.

TurboFramesは装飾をしない • 左パターンはクリック後しばらく固まる • 右パターンはまず空のダイアログが出てくる • ユーザーが困らないのは右パターン

41.

TurboFramesは装飾をしない

42.
[beta]
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>

43.
[beta]
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>

44.
[beta]
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>

45.

Eager/Lazy-loading? • TurboFramesを使った分割遅延読み込み • ページを構成する要素を、違うエンドポイン トから取得してturbo-frameタグの中にいれ ることができる

46.

Eager/Lazy-loading? https://turbo.hotwired.dev/handbook/frames

47.

TurboFramesは装飾をしない • 事前に表示できるDOMは表示する • 大きなパーツをTurboFrames化しない • TurboFramesのEager/Lazy-loadingを使っ て代替コンテンツを表示する

48.

CacheとPreviewはあまり うれしくない

49.

CacheとPreviewはあまりうれしくない • TurboDriveにキャッシュという仕組みがある • DOMを覚えておき、再訪問した時に表示する • 戻る/進むはCache、遷移はPreview

50.

CacheとPreviewはあまりうれしくない ダイアログを閉じてから画面遷移し、戻るボタンを押すと……

51.

CacheとPreviewはあまりうれしくない • StimulusとかでDOMをいじっていると意図 しない状態が残ってる • データ更新があった場合に正しくない画面を 描写している • 基本的にあまりうれしくない

52.

CacheとPreviewはあまりうれしくない 完全にStaticでもない限りは切っておこう <head> <meta name="turbo-cache-control" content="no-cache"> </head>

53.

代替コンテンツを表示しよう

54.

代替コンテンツを表示しよう • Eager/Lazy-loadingは、通信中に代替コンテ ンツを表示できる • <turbo-frame>の中に書かれているDOM • 適切に設定して、ユーザーに読み込みを通知 する

55.

代替コンテンツを表示しよう • スケルトンスクリーンが良い • レンダリングする予定のDOMをフレームだけ 表示する奴 • Cacheを切ったので、ユーザーに読込中を提 示するために必要になる

56.

代替コンテンツを表示しよう 同時にページをリロードして、挙動を比較

57.

代替コンテンツを表示しよう • 左パターンは読込中に空白の画面がでる • 右パターンは読み込みアニメーションが出る • 右はスケルトンスクリーン

58.

代替コンテンツを表示しよう

59.

代替コンテンツを表示しよう <% 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 %>

60.

代替コンテンツを表示しよう <% 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 %>

61.

代替コンテンツを表示しよう <% 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のリクエスト判定

62.

代替コンテンツを表示しよう <% 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にする

63.

代替コンテンツを表示しよう <% 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 %>

64.

代替コンテンツを表示しよう <% 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 %>

65.

代替コンテンツを表示しよう <% 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 %>

66.

代替コンテンツを表示しよう • Eager-loadingの代替コンテンツを活用 • turbo-framesタグの中のDOMがまず描写さ れる • srcの読み込みが終わったら置き換えられる

67.

代替コンテンツを表示しよう • Eager-loadingを使って自身を呼び出す • View内部で、Turboリクエストかどうかを判定する • 通常であればスケルトンを、Turboであれば実際のコン テンツを返す

68.

代替コンテンツを表示しよう • まずスケルトンを返すと、レスポンスが速い • キャッシュをオフにした時間を取り返せる • 重いコンテンツはガンガンEager-loading!

69.

代替コンテンツを表示しよう 🙌

70.

まとめ

71.

まとめ • TurboはRESTのメンタルモデルを保っている • 一方で、読み込みのフィードバックは壊れる • ユーザーに何が起こっているかをイベントや 代替コンテンツで通知しよう

72.

まとめ Turboのイベント一覧リファレンスとかを見ると知見があります https://turbo.hotwired.dev/reference/events

73.

まとめ • レイテンシが速い場所にデータ置こう • HerokuのUSは日本から遠い • グローバル展開するときは大事だね

74.

まとめ • 来年はデザインの刷新を予定しています • よりモバイルで使いやすいものにします • また来年、松山でお会いしましょう