概要
Popcorn Web はサーバーで描画し、ハイドレーション層、クライアントサイドルーター、 クライアント状態ストアを持ちません。メニューを開く、行をその場で編集する、ページ遷移を 滑らかにするといった操作には、ブラウザの API またはアプリケーション側で選んだ ライブラリを使います。
必要な動作をブラウザが備えているなら、まず標準機能を使ってください。フレームワークの ランタイムを増やさず、無関係なスクリプトの読み込みに失敗しても動き続けます。それでも 足りない部分にだけクライアント側のコードを加えます。画面の大半が長時間保持する クライアント状態に依存するなら、サーバー描画中心の構成そのものが適さない可能性があります。
| 段 | 足すもの | できること | できないこと |
|---|---|---|---|
| ブラウザ | 何も足さない | ページがすでに持っているものを、出す・隠す・配置する・遷移させる・検証する | サーバーが送っていないことを知る |
| CSS コンポーネント | Tailwind プラグイン 1 つ | そのマークアップに見た目とテーマを与える | 振る舞いを変える |
| サーバーフラグメント | ドキュメントシェルに swap ライブラリ 1 つ | サーバーにしか作れないマークアップで領域を差し替える | 往復なしで動く |
| 自作の島 | 自分で書く JavaScript | 往復では答えられないローカル状態とイベント | 無料で手に入る —— 失敗も含めて自分の持ち物です |
規則は、その操作を表現できるいちばん下の段を取ること、そして下の段では本当に
表現できないときにだけ上がることです。慣れているというのは理由になりません。
popover 属性で作ったドロップダウンとコンポーネントライブラリで作った
ドロップダウンは読み手には同じものに見えて、壊れうるのは一方だけです。
| やりたいこと | 成立する最も安い段 | 場所 |
|---|---|---|
| モーダル、確認 | <dialog> |
ブラウザ標準の部品 |
| ドロップダウン、ツールチップ | Popover API | ブラウザ標準の部品 |
| アコーディオン、開閉 | <details name> |
ブラウザ標準の部品 |
| トースト、通知 | popover="manual" |
フラグメントと島 |
| 送信前の入力フィードバック | 制約検証、:user-invalid |
フォーム |
| 送信後のフィールドエラー | サーバーがフォームを描き直す | フォーム |
| 小さな候補リスト | <datalist> |
フォーム |
| ページ間の連続性 | ビュートランジション | ナビゲーション |
| 一瞬で切り替わる遷移 | Speculation Rules | ナビゲーション |
| 絞り込み、インライン編集、ライブなリスト | フラグメントの swap | フラグメントと島 |
| 宣言的なサーバーフラグメントの swap | htmx | htmx の統合 |
| 中身をサーバーが用意するダイアログ | <dialog> への swap |
フラグメントと島 |
| 生成に時間のかかる領域 | await 境界 |
非同期レンダリング |
| 誰も見ていなくても変わる領域 | live 境界 | ライブレンダリング |
| クライアントだけの状態、ドラッグ、canvas | カスタム要素 | フラグメントと島 |
| 自動解放されるインスタンス固有の JavaScript | コンポーネントスクリプト | コンポーネントスクリプト |
| コンポーネント内に閉じたクライアント状態 | 島の中の React ルート | React の統合 |
| 楽観的更新、オフライン編集 | —— | このフレームワークの領分ではありません |
先に動くのは誰か
Section titled “先に動くのは誰か”この表の行はどれも、読み手が動くところから始まります。クリック、キー入力、 ホバー。ですが画面が必要とするもののうち2つは、そこから始まりません。そして この2つこそ、このフレームワークがクライアント描画のフレームワークより多く 持っている場所です。
1つめは、サーバーがすぐには用意できない領域です。読み手はもうページを要求して
いて、遅れているのはページのほうです。コンポーネント指向のフレームワークなら、
領域ごとのローディング状態で答えます。状態を持つ場所、埋めるための fetch、自分で
書いたスピナー、そして依存が連鎖すればウォーターフォール。await 境界の答えは、
中身のすぐ隣に書いた fallback です。シェルと fallback は即座にコミットされ、
遅い領域は同じレスポンスの中でデータが確定した順に自分を置き換え、その裏の遅い
依存どうしは順番待ちではなく重なって走ります。この過程を説明するクライアントの
状態は、ひとつもありません。何も実行しないクローラや CLI クライアントには確定済みの
ドキュメントが渡るので、索引に載るのは「読み込み中」ではなくページの中身です。
fallback がそのまま残るのはスクリプトを切ったブラウザだけで、fallback を読むに足る
文言にしておく理由はそこにあります。
2つめは、誰もページに触っていない間に変わる値です。キューの深さ、終わったビルド、
他人が送ったメッセージ。ここでの反射は hx-trigger="every 2s" で、これは動きます。
ただし、答えがたいてい「何も変わっていない」である問いを投げ続ける、という代償と
引き換えです。開いているタブはそれぞれ自前のタイマーでその代償を払い、間隔は2つの
失敗のあいだの当て推量になります。短すぎればリクエストの無駄、長すぎれば数字が古い。
live 境界は向きを逆にします。Go のソースは言うことができたときに yield し、サーバーは
その領域だけを描き直し、それを運ぶ接続はもう開いています。テンプレートは同じ
await 節のままで、ブラウザはすでに読み込んでいるランタイムで配信を適用します。
スクリプトが動かない読み手にも、その領域の実際の描画が 1 回は見えます。ドキュメントが
終わる前に、ソースの最初の値をコミットしているからです。
| 動いたもの | 誰が要求したか | 何を書くか | 場所 |
|---|---|---|---|
| 読み手 | 読み手 | swap、あるいは何も | このセクション |
| ページの生成が遅い | 読み手が1回 | async パラメータ、await, fallback |
非同期レンダリング |
| データが独りでに | 誰も | external live のソースと同じ await 節 |
ライブレンダリング |
どちらもタダではなく、コストの落ちる先はフロントエンドの勘が向く場所とは違います。
await 境界は Content-Length を手放し、圧縮は 1 回ではなく境界ごとにフラッシュ
されます。live 境界は画面あたり 1 本の接続と、再接続ごとのページ実行 1 回を払います。
クライアントの複雑さではなくサーバーの負荷で、200 人が見ているダッシュボードは
「2 秒ごとに 200 回のポーリング」ではなく「tick ごとに 200 回の描画」になります。
設計するうえで効いてくる規則がひとつあります。live な領域は、読み手が別のことを
している最中に、サーバーの都合で置き換わります。だから live な領域には出力を置き、
入力は置きません。live 節の中の form、input、textarea、select は実行時の
不意打ちではなく生成エラーです。フォームは境界の外、変わるデータは境界の中 ——
フォーカスと選択範囲が保たれるのも同じ切り分けのおかげで、これが助言ではなく
コンパイラの規則になっている理由でもあります。
見た目は別の軸
Section titled “見た目は別の軸”ここまでの話は、それがどう見えるかを何も決めていません。スコープ付きの
コンポーネントスタイルと Tailwind はスタイリングで扱います。
daisyUI はその上に乗る CSS だけのコンポーネントクラス群と、data-theme による
テーマ切り替えです。
このセクションにとって重要なのは、daisyUI が自分で書いたマークアップに
スタイルを当てるのであって、マークアップを置き換えないという性質です。
class="modal" を足しても <dialog> は <dialog> のまま —— トップレイヤーも
Esc もフォーカスの扱いもそのままです。構造とアクセシビリティの意味づけは自分の
側に残り、だからこそブラウザの段と CSS の段は二者択一ではなく同時に使えます。
フレームワークが出すもの
Section titled “フレームワークが出すもの”5 つあり、以降のページはそのどれにも寄りかかります。
pw.WriteHTMLFragmentはテンプレート 1 つだけを描画します。最初に描いたのと 同じコンポーネントで領域を描き直せる、ということです。契約は レスポンスに、それだけで組んだ完全なアプリケーションはexamples/htmx_fragmentにあります。- 非同期レンダリングは、遅い部分が後から届くページをストリーミングします。 クライアント側のローディング状態がまるごと 1 種類なくなります。 非同期レンダリングを参照してください。
- ライブレンダリングは、読み手がページを開いているあいだ 1 つの領域を描き直し 続けます。代わりに置かれていたはずのポーリングがなくなります。 ライブレンダリングを参照してください。
- ブラウザ側ハンドラの、検査された名前。コンポーネントのスクリプトブロックが
マークアップから呼んでよい関数を返し、
on-click="increment"がそれを発火させる 要素の上で名指しします。名前を変えれば、クリックが黙って何もしなくなるのではなく 生成が落ちます。 コンポーネントスクリプトを 参照してください。 - 変更操作の、検査された宛先がページツリーの中にあります。
server-actionは Go のハンドラ名を生成されたエンドポイントへ解決するので、関数名を変えたときに 失敗するのは本番のクリックではなく生成です。フォームに置けば、ランタイム抜きでも そのまま送信されます。レスポンスではなく答えが欲しいスクリプトは、代わりに普通の Go の関数を宣言してawait actions.getUser({ id })と呼びます。両端に型が付くので、 手書きのfetchとその周りのデコードが要らなくなります。 サーバーアクションを参照してください。
この階段の残りは標準的なウェブプラットフォームの作業です。フレームワークは swap ライブラリを名指ししませんし、CSS プラグインを包みませんし、どのルートも自分を 呼んだクライアントライブラリが何かを知りません。
ブラウザ対応について
Section titled “ブラウザ対応について”ここに出てくる機能には、前提にしてよいほど古いものと、そうでないものがあります。 各ページはその区別を示し、区別された側は同じ使い方をします。すでに動いているものの 上に重ねる強化として使う、ということです。走らなかったビュートランジションは ただの遷移を残し、起こらなかった prerender はただのクリックを残します。
