Hotwire/Honoなウェブアプリのアーキテクチャ
React Notes: MarkdownエディタのUIを作る
「React Notes」というReact Server Components(RSC)が発表された時期にReactチーム[1]やVercel[2]が公開していたブログ投稿デモサイトがあって、それをHotwireとHono/JSXで作ってみることでRSCなしに似たようなUXが作れるっていうのを示せるのではと思って、今クローンを作ってみています
現在はテキストエリアにMarkdownを入力するとプレビューをしてくれて、保存→更新の画面遷移がひととうりできるという部分のUIだけ先に試しに書いてみて以下にデプロイしました
ソースコードがここにあります
- SSRな部分をHono/JSXのテンプレート処理系に寄せて
- クライアントサーバー通信と画面更新のコードはHotwire/Turboで簡略化
- イベントハンドラな部分はHotwire/Stimulusの補助を受けてVanilla JSで書く
という、アーキテクチャなのでサーバーとして動くCloudflare Workersのプログラムからハイドレーション関連のコードをなくして軽量にできました
これをCloudflare Pagesにデプロイできるので、Next.jsやRemix等とはまた違った特性が出せるのではないかと考えています
従来HotwireはRuby on Railsに統合して使っていたんですけど、バージョンアップにより単にJavaScriptライブラリとして色んな環境から使いやすくなりました
それに加えてHono/JSXがサーバーサイドのテンプレート処理系としてよくできているし、React Server/DOMを真似したAPIが生えてて(JSXとは・・)泥くさい部分をカバーしてくれたりで使い勝手がいいです
もちろんRSCと比べてHotwireの方が優れているというわけでもなくて、クライアントサイドのチャンクを段階的に読み込んだり、コンポーネント単位で非同期にストリーミングしてキャッシュを効かせたりといった戦略はRSC(Next.js)の方が得意だろうし、Hotwireのidキーを軸にDOMが自動で書き換えられていくのがReactと比べて複雑なDXを引き起すトレードオフもありそうだな思います
用語の整理
Hono
新しめのサーバーサイドJavaScriptフレームワーク。Express, Fastify, NestJSなどと同じレイヤー
Node.js以降のJavaScriptランタイム(Deno, Bun, Clodflare Workers等)で快適に動くので注目されている。
Hono/JSX
Hono内部で実装されているテンプレートエンジンが発展したJSX処理系。HonoでAPIサーバーだけでなくWebページも構築するニーズが出てきて作られた
Honox
Honoに入ってない実験的な機能が詰ったフルスタックなWebフレームワーク。
Next.js、RemixやDeno Freshなどと同じレイヤー
Viteを起点に配置したファイルから複数のHonoのルーターをインスタンス化して統合してくれたり、Hono/JSXでSSRしたコンポーネントを選択的ハイドレーション(Islandアーキテクチャ)してonClick
を動くようにしたり、任意の個所にファイル配置したり関数渡したりしてカスタマイズしたりするプラガブルな設計になってる
Hotwire
Turbo/Stimulus/Stradaというライブラリたちを総称した名前
Ruby on Railsの開発元が作っていてRailsでアプリケーションを作ると自然と導入されてくるのでプログラミング初学者の人がよくハマっているのを見かける(ただベテランもハマってる)
Hotwire/Turbo
サーバーにリクエストを送信して返ってきた結果で画面を更新するのをHTML内DSLでやってくれるJavaScriptライブラリ。Htmxなどと同じレイヤー。
ソースコードをTypeScriptからJavaScriptに書き換えたらなぜか炎上したことで有名。ビルドツールのvercel/turboとは全然関係ない
Turbo Drive, Turbo Frame, Turbo Streamという下位概念を持つけどシンプルに「Drive=全体(turbolinks)、Frame=特定の個所(独自iframe)、Stream=複数の個所(pushも可)」と覚えてる
Hotwire/Stimulus
DOMにVanilla JSをアタッチしてクライアントサイドの振舞いだけを書けるようにするライブラリ
jQuery, Alpine.jsなどと同じレイヤー(個人的にはWeb Components系のライブラリも含めたいのですが@lit-labs/ssrなど最近はリッチな機能を持つものも登場している)
Hotwire/Strada
TurboのiOS/Android WebView向けSDKのTurbo NativeにStimulusのようにネイティブアプリ内部実装をくっつけられるライブラリ
Capacitor(Ionic)などと同じレイヤー
四天王のうち最弱らしく使われているのを見たことがない、けどHEYアプリで37signalsの人達は本番投入しているらしい
ソースコードとWORKAROUNDの解説
先にあげたnotes-hotwireを作るうえで工夫した点やWorkaroundを解説
- Hono/JSX
- Honox
- Hotwire/Turbo Drive
- Hotwire/Stimulus
が出てくる
Turbo Frame, Turbo Streamはまだ使ってないけど、たぶんそのうち使います
全体図
├── app
│ ├── client.ts
│ ├── components
│ │ ├── note-editor.tsx
│ │ └── note-viewer.tsx
│ ├── controllers
│ │ └── note_controller.ts
│ ├── global.d.ts
│ ├── islands
│ │ └── dummy.tsx
│ ├── routes
│ │ ├── _renderer.tsx
│ │ ├── index.tsx
│ │ └── note
│ │ ├── [id]
│ │ │ ├── edit.tsx
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── server.ts
│ ├── styles
│ │ ├── note-preview.css
│ │ └── tailwind.css
│ └── types.ts
├── package.json
├── postcss.config.js
├── public
│ └── static
│ └── favicon.ico
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
TailwindCSS
TailwindCSSを使えるようにviteを設定している
vite buildしてできたcssファイルを import styles from "../styles/tailwind.css?url";
で読み込ませる
<Script />
についてはhono/cssで書いたスタイルを展開するもので、このプロジェクトではMarkdownのプレビューのスタイルを単一スコープで管理するために使っています
以下からcssファイルを適用しています
Honox: client.ts (WORKAROUND👷)
client.ts
はクライアントサイドのエントリーポイントで、基本的にHono/JSXやReactのハイドレーションに使う想定になっている
なので動作させるためには現在「islandコンポーネントをロードしていること」という制約がある
今回はislandコンポーネント全無視でHotwireでHTML中心に書くので、各ページにおまじないを入れてる
中身は空
app/components/: Viewコンポーネント
app/routes/
に記述したJSX(HTML)がある程度意味を持ったらコンポーネントに切り出してる。
Reactコンポーネントで想像する非同期で状態管理を持つ部品ではなくて、実質サーバーサイドのpartialsテンプレート片として運用できる。Hono/JSXなのでツリー構造として組合せできるし、TypeScriptの補助を受けられるのでerbより気に入ってる
(※状態管理はサーバーサイドでやるか、Stimulusでクライアントサイドだけに押し込める)
app/controllers/: Stimulsコントローラー
Viewコンポーネントでdata-controller="note"
という要素があるのはこのコードが接続される
Viewコンポーネントとコントローラーが多対一になっている部分が注目できると思う
│ ├── components
│ │ ├── note-editor.tsx
│ │ └── note-viewer.tsx
│ ├── controllers
│ │ └── note_controller.ts
Viewとしては2つのパターンを持ちつつも、ふるまいとしては同じ意味を持つ処理に紐づくというのをHTML要素を起点にStimulsが自動で解決してくれるのでこう書いた方が自然になる
コントローラーはエントリーポイントで以下のように登録している
(※import "@hotwired/turbo"
はTurbo Drive, Frameを動作させるのに必要)
app/routes/: Honoxのファイルベースルーティング
Next.jsやRemixのように規約に従ったファイルを配置しておくと自動でルーテイングの定義に反映される
│ ├── routes
│ │ ├── _renderer.tsx
│ │ ├── index.tsx
│ │ └── note
│ │ ├── [id]
│ │ │ ├── edit.tsx
│ │ │ └── index.tsx
│ │ └── index.tsx
GET /
新規にNoteを投稿するformを出すページ
POST /note
formのactionのPOST先
status=303
にするのはTurboが期待するレスポンスだから
After a stateful request from a form submission, Turbo Drive expects the server to return an HTTP 303 redirect response, which it will then follow and use to navigate and update the page without reloading. https://turbo.hotwired.dev/handbook/drive#redirecting-after-a-form-submission
GET /note/:id
投稿されたNoteを閲覧するページ
DOMPurify.sanitize (WORKAROUND👷)
悪意のあるユーザーが<script>タグなどを保存して第三者に実行させようとするのを防ぐために、DOMPurify.sanitize(html)
は本来サーバーサイド側でかけてからレスポンスに出力したい。しかしCloudflare Pages(Workers)でdompurifyやsanitize-htmlを動かすのができなくてしかたなくクライアントサイドで描画前にフィルタしている。
ここはサーバーサイドで処理を追加してなんとかしたい
GET /note/:id/edit
投稿したNoteを編集するページ
PUT /note/:id
編集をsubmitするとこのエンドポイントにデータが送信される。あとはPOSTの時と同じだがTurbo Driveでform#submitをする時にFetch APIでHTTPメソッドをPUTに置き換えるためにformmethod="put"
を入れている
他の方法としてはform自体はブラウザの機能をそのまま使い <input name="_method" type="hidden" value="put" />
のパラメータをPOSTで送信してサーバーのリクエストミドルウェアのレイヤーで置き換えでハンドリングするというのもある
まとめ
本記事ではHotwire/Honoを使ったWebアプリのフルスタックなアーキテクチャに入門しました
ReactのRSCと比較して1点だけお勧めなポイントを上げるとすれば、サーバーサイドで実行するコードサイズを小さくできる余地があるのでFaaSやedge runtime系のライフサイクルと相性が良さそうな点でしょうか
Hotwire/Hono間でサーバーサイド/クライアントサイドと明確に境界ができるため、双方のバンドルサイズ最適化をコントロールしやすいです(巨大ライブラリを埋め込んだら別ですが……)
著者はCloudflare Pages(Workers)もFaaSの一種だと思っているので、本記事のアーキテクチャはCloudflareスタック向きであるとは言えそうです
また本記事の範囲はHtmxやAlpine.jsでも代用できると思います[3]
Cloudflare Workers and micro-frontends: made for one anotherにあるような水平分割可能なアーキテクチャにも応用できそうですね
とはいえRemixやSvelte, Solid, Qwikのメタフレームワークレイヤーなどとは比較していないのでまだ実験的な段階ですが……
-
Introducing Zero-Bundle-Size React Server Components – React https://react.dev/blog/2020/12/21/data-fetching-with-react-server-components ↩︎
-
vercel/server-components-notes-demo: Experimental demo of React Server Components with Next.js. Deployed serverlessly on Vercel. https://github.com/vercel/server-components-notes-demo ↩︎
-
Hono + htmx + Cloudflareは新しいスタック https://zenn.dev/yusukebe/articles/e8ff26c8507799 ↩︎
Discussion