サーバで描く
受注一覧をサーバで描く画面です。ブラウザに届いた HTML には表が入っているので、ページのソースに出ますし、JavaScript を無効にしても表示されます。ブラウザ側では、同じ画面が領域を描き直さずに引き取ります。
view は一つ、使うのは両側
画面は一つのモジュール(SsrScreen)で、サーバとブラウザの両方が import します。view は一つなので、二つの実装を同期させる必要がありません。
このモジュールに起動処理はありません。import したモジュールの最上位の文は import した時点で実行されるので、App hydrate: をここに書くとサーバでも実行されてしまいます。
サーバ側
ランタイムはブラウザなしで Node の上で動きます。App page: でデザイナの HTML を読み、App mount:at: で画面を描き、App document がページ全体を返します。
Server class >> render: html data: json = (
App page: html.
screen := App mount: (Screen fromJson: ((Json parse: json) orElse: [Json null])) at: '#app'.
page := App document replaceAll: '</body>'
with: '<script id="lps-data" type="application/json">' , json , '</script></body>'.
screen ! (Done fields: {}).
page ).
描画に使ったデータは JSON としてページに埋め込みます。ブラウザのアクタが同じ状態から始められるようにするためです。描いたアクタは返したあとに停止させます。要求ごとに増えていくものを残さないためです。
一回の描画は約 1 ミリ秒です。開発用のサーバは、組んだモジュールを一度読み込み、要求ごとに Server render:data: を呼びます。プロセスの起動もコンパイルもしません。
ブラウザ側
App hydrate:at: が既にある領域を引き取ります。view が返した VNode と既存のマークアップを並べて辿り、タグが一致するノードはそのまま使ってイベントリスナを付けます。一致しないノードだけ作り直します。
seed := ((Doc getElementById: 'lps-data') map: [:e | e textContent]) orElse: ['{}'].
screen := App hydrate: (Screen fromJson: ((Json parse: seed) orElse: [Json null])) at: '#app'.
引き取るときに属性は書きません。同じ view から生成したマークアップなので既に一致しており、value を書くと、スクリプトの読み込み中に利用者が入力した文字が消えるためです。
App document は、デザイナの見本の要素を <template id="lps-original"> に残します。ブラウザ側の Html clone: はそこから複製します。
前提
ブラウザのアクタは、サーバと同じ状態から始まる必要があります。状態が違うと view が食い違い、領域を引き取れません。
テスト
ヘッドレスで 5 件、本物のブラウザで 6 件です。ヘッドレスでは App document の形、引き取ったあとのマークアップ、入力した文字がハンドラに届くこと、複製が template から来ることを確認します。ブラウザでは、同じノードが再利用されること、入力した文字とフォーカスが残ること、リスナが付くことを確認します。