zfb
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

サーバーレンダリング

ページ全体をエスケープ済みの HTML にレンダリングし、明示的な島ルートの内側にだけ専用マーカーを追加します。

ページをレンダリングする

サーバーエントリは @takazudo/zfb/zudo-react/server です。renderToString は同期的な記述ツリーを HTML にレンダリングします。文字列、数値、空の値、配列、記述、スカラー値を持つリアクティブな子を受け付けます。レンダラーへ直接渡した文字列はテキストとしてエスケープされます。

import { renderToString } from "@takazudo/zfb/zudo-react/server";

const html = renderToString(
  <html>
    <head><title>Example</title></head>
    <body><main><h1>Hello</h1></main></body>
  </html>
);

この呼び出しで文書全体を生成します。レンダラーは doctype を追加しません。テキストではアンパサンドと山括弧、属性ではさらに二重引用符をエスケープします。prop の挿入順を保って属性を二重引用符で囲み、template 要素には開始タグと終了タグを出力し、head の終了タグは小文字でそのまま出力します。HTML の空要素には終了タグがなく、子や rawHtml は使えません。SVG の空要素以外は開始タグと終了タグを出力します。

静的な出力では、明示的な島の境界の外にランタイムのコメントや属性は付きません。通常のコンポーネント、配列、Fragment の境界でも区切りや空白は増えません。このため、ハイドレーション用のプロトコルマーカーなしでページをレンダリングできます。

zfb は最上位の非同期ページ関数を待ってからレンダラーを呼べます。一方、レンダリングツリー内の非同期コンポーネントや Promise は ZR_ASYNC_COMPONENT で拒否され、ランタイムは入れ子の処理を待ちません。

島を明示的に指定する

レンダリングしたサブツリーをブラウザで有効化する場合は islandRoot を使います。これは不活性なラッパー記述を作り、コンポーネントのセットアップと prop のシリアライズはラッパーをレンダリングするときにだけ行われます。

import { islandRoot, renderToString } from "@takazudo/zfb/zudo-react/server";

const identity = { component: "Counter", build: "build-7" };
const root = islandRoot(<Counter />, {
  identity,
  when: "visible",
  fallback: <p>Loading the counter</p>
});

const html = renderToString(<main>{root}</main>);

ラッパーは div です。コンポーネントマーカーは data-zfb-island または data-zfb-island-skip-ssr のどちらか一つだけです。さらに data-when、data-zfb-transport="json/1"、data-zfb-protocol="zudo-react/1"、空ではない data-zfb-build の識別子、props が空でも data-props を持ちます。属性はこの順に出力され、media モードの場合だけ data-when と transport の間に data-media が入ります。data-when のデフォルトは load で、idle、visible、media も使えます。media には空でない data-media クエリーが必要です。skip-SSR オプションは専用マーカーを選び、子を実行せずに identity と props を検証し、内部のハイドレーションマーカーなしで fallback をレンダリングします。

zfb がレンダリングする島では、コンポーネント名とビルドの識別子を生成された zfb メタデータおよびクライアントの RootOptions に一致させます。

呼び出し元が所有するクライアント mount では、独自の空でない安定したビルド文字列を選べます。ラッパーと RootOptions.identity.build の値は一致させてください。詳しくはハイドレーションを参照してください。

zfb が生成したサーバー島では、スキャナーのマーカーは定義元の関数名であり、displayName は読み取りません。サーバーレンダリング時、displayName は未設定か、その関数名と一致している必要があります。一致しない値ではターゲット名が変わらず、ZR_ISLAND_IDENTITY で失敗します。無名または未登録のコンポーネントも島の子にできません。スキャナーへの登録、スケジュール、永続化には既存の zfb の島連携を使ってください。詳しくは島のガイドを参照してください。

ラッパーは zfb が保持します。zudo-react のマーカーはラッパー内の範囲だけを示すので、周囲の文書にマーカーは付きません。各ルートは独立したマーカー番号を持ち、作成者がグローバルな要素 ID や独自のコンポーネントレジストリーを用意する必要はありません。

島の内部サブツリーをレンダリングする

呼び出し元がラッパー属性を自分で用意する場合、renderToString は島の identity オプションも受け付けます。この場合、ラッパーは出力せず、同じ内部範囲マーカーを出力します。一致するコンテナーのメタデータを用意するのは呼び出し元の責任です。

const inner = renderToString(<Counter />, { island: identity });

このオプションは島内部のマーカーだけを出力します。静的スキャナーへのコンポーネント登録や、クライアント mount の境界定義は行いません。サーバーレンダリングする島には、島 API ガイドで説明する zfb のサポート済み境界を使います。クライアント専用のホストはラッパーを所有し、ハイドレーションで説明するように mount を呼び出せます。

Props は JSON で転送する

このシリアライズ規則は、サーバーレンダリングの islandRoot と data-props 属性に適用されます。クライアントの mount を直接呼ぶホストは、コールバックや子の記述を含む props をメモリ上で渡します。詳しくはハイドレーションを参照してください。

serializeProps は JSON テキストを返します。レンダラーが data-props として出力する際に、HTML 属性として一度エスケープします。転送できるのは単純なデータレコード、欠けのない配列、文字列、真偽値、null、有限数だけです。自身が持つ列挙可能な文字列キーのデータプロパティで、値が undefined のものはどの階層でも省略されます。明示的な null は保持されます。

import { serializeProps } from "@takazudo/zfb/zudo-react/server";

serializeProps({
  locale: undefined,
  currentSlug: undefined,
  metadata: { title: "Card", description: undefined },
  records: [{ description: undefined }, { description: null }],
});
// {"metadata":{"title":"Card"},"records":[{},{"description":null}]}

サーバーコンポーネントにはシリアライズされるものと同じ正規化済み props が渡るため、SSR とハイドレーション時の Object.hasOwn の結果は一致します。配列内のレコードにも同じ省略規則を適用しますが、配列要素を削除したり移動したりしません。undefined の配列要素と配列の穴はエラーです。null は値として保持されます。複数回参照される値は値としてシリアライズされます。循環参照、関数、symbol、bigint、非有限数、クラスインスタンス、ランタイムの記述、signal、computed 値も拒否されます。

検証は JSON シリアライズより前に行われます。配列には欠けがなく、追加の名前付きプロパティや symbol プロパティも付けられません。レコードは通常のものでも null プロトタイプでもよく、自身が持つ列挙可能な文字列キーのデータプロパティだけが許可されます。アクセサー、列挙不可のフィールド、独自の toJSON、symbol、プロトタイプに影響するキーは、呼び出されることなく拒否されます。props は境界を越えて生きた signal の同一性を運びません。クライアントのセットアップ中に signal を作ってください。

レンダリング失敗の読み方

zfb build や zfb dev のサーバーレンダリングが要素を拒否すると、エラーには 3 種類の情報が含まれます。次の順に読んでください。

ZR_TAG は現在の HTML または SVG 語彙にないタグを示します。ZR_ATTRIBUTE は未対応の属性名、または適用される値の規則に合わない値を示します。これにはカスタム要素の文字列のみの規則も含まれます。全一覧と正確なメッセージ例は対応する HTML と SVG の語彙を参照してください。

[zudo-react] ZR_PROP_DIALECT render static-render root: use `autocomplete` instead of `autoComplete` — at components/search-field.tsx:4:7
[zfb-runtime] render threw for "/": ZR_PROP_DIALECT: input.autoComplete (use `autocomplete` instead of `autoComplete`) at root in static render
TypeError: ...
    at fail (file:///zfb/bundle.mjs:3505:23)
  • 記述した位置。 — at components/search-field.tsx:4:7 は、失敗した JSX 要素のソースファイル上の行と列です。要素の呼び出しを、ビルドのソースマップと内容が一致するプロジェクトのファイルまで戻せた場合にだけ表示します。事前に zfb check を実行する必要はありません。

  • 構造上の文脈。 1 行目には安定したコード、コンポーネント(ページのレンダリングでは static-render、それ以外は島のコンポーネント名)、要素のパスも含まれます。React 形式の prop では、代わりに使う綴りも示します。この部分は常に表示されます。

  • 生成されたスタック。 file:///zfb/bundle.mjs:… のフレームは、生成されたサーバーバンドル内の位置です。これは zfb のレンダラーの位置であり、要素の位置ではありません。この番号を頼りに編集しないでください。

h() で書いた要素、依存パッケージ内で作られた要素、プリプロセッサーが書き換えたコードには、信頼できるソース位置がありません。これらについては推測した行を出さず、記述した位置なしで構造上の文脈を表示します。デバッグの手がかりとして — internal frame node_modules/…、<generated>/…、external/… のような内部フレームを付けることがありますが、これはあなたのソースの位置ではありません。1 行目には props、raw HTML、ローカルの絶対パスは含まれません。

zfb ビルドの外で Island をレンダリングする

v3.0.0 から、Island はレンダリングの前に子を検証します。ビルド識別子と登録済みの島の名前は、zfb のサーバーバンドルが globalThis.__zfb に設定するメタデータから読み取ります。別のバンドラーや素の Node スクリプトでレンダリングするページ(たとえば静的エクスポート用に自分のレイアウトをレンダリングするパッケージ)には、このメタデータがありません。そのため、すべての Island が次のようなエラーで失敗します。

ZR_ISLAND_IDENTITY: ThemeToggle has no build identity

withIslandTestContext で識別子を与える

@takazudo/zfb/zudo-react/testing の withIslandTestContext は、同期コールバック 1 回の間だけこのメタデータを設定し、終了後に元の globalThis.__zfb を復元します。レンダリングが例外を投げた場合も復元します。サブパス名に反して、zfb ビルドの外で SDK の Island 境界をレンダリングするためのサポートされた方法です。

import { Island } from "@takazudo/zfb";
import { renderToString } from "@takazudo/zfb/zudo-react/server";
import { withIslandTestContext } from "@takazudo/zfb/zudo-react/testing";
import { SidebarToggle } from "./sidebar-toggle";

export function renderLayout(): string {
  return withIslandTestContext({ components: [SidebarToggle], build: "my-site-1" }, () =>
    renderToString(
      <body>
        <Island when="idle">
          <SidebarToggle />
        </Island>
      </body>,
    ),
  );
}
  • components には、コールバック内で Island の子として使うすべてのコンポーネントを、配列か、値がコンポーネントのレコードで渡します。名前はコンポーネントの displayName、なければ関数名から取ります。一覧にない子を持つ Island は ZR_ISLAND_IDENTITY: <name> is not registered by the scanner で失敗します。

  • build は各ラッパーの data-zfb-build 属性に書かれる文字列です。デフォルトは "test" です。空でない安定した値を渡してください。

  • JSX は @takazudo/zfb/zudo-react を JSX import source にしてコンパイルしてください(例: esbuild の jsxImportSource)。別の JSX ランタイムで作った要素は zudo-react の記述ではないため、Island は ZR_ISLAND_CHILD で拒否します。

  • コールバックは同期でなければなりません。登録は共有のグローバルに置かれるため、promise を返すコールバックは ZR_ISLAND_TEST: context callback must be synchronous で失敗します。データの読み込みはコールバックに入る前に済ませてください。

minify したバンドルで関数名を保持する

Island は子を関数の name プロパティで識別します。zfb 自身のバンドラーは常に関数名を保持するので、zfb のビルドで名前が失われることはありません。利用側のバンドラーが関数名の保持なしで minify すると関数名が変わり、検証に失敗します。displayName を上書きしている場合は、minify 後の名前と衝突します。

ZR_ISLAND_IDENTITY: dc conflicts with displayName SidebarToggle

同じコンポーネントを withIslandTestContext に渡すと、ヘルパーが先に ZR_ISLAND_IDENTITY: invalid component name dc で拒否します。displayName がなければレンダリングは成功しますが、島のマーカーには minify 後の名前(data-zfb-island="dc")が入ります。元のソースからビルドしたクライアントは、この名前を認識しません。

単独でレンダリングするバンドルを作るバンドラーで、関数名の保持を有効にしてください。

  • esbuild: JavaScript API では keepNames: true、コマンドラインでは --keep-names。

  • Rolldown(Vite 8 を含む): output.keepNames: true。Vite 8 では build.rolldownOptions.output.keepNames として設定します。

  • Terser(Vite の build.minify: "terser" を含む): keep_fnames: true。Vite では build.terserOptions で渡します。

  • その他の minifier: 関数名を保持するオプションを有効にします。

バンドラーを変更できない場合の代替策は二つあります。島のコンポーネントから displayName の上書きを取り除くとエラーは止まりますが、マーカーには minify 後の名前が入ったままです。元のソースからビルドしたクライアントがその出力をハイドレーションしない場合にだけ使ってください。それ以外では、単独ビルドで Island を使わずに子を直接レンダリングしてください。Island なしでレンダリングした子は静的な HTML になり、ハイドレーションされません。

zfb のクライアントでハイドレーションする

withIslandTestContext が満たすのはサーバー側の検証だけです。zfb がビルドしたクライアントバンドルでページをハイドレーションする場合、各ラッパーのコンポーネント名と data-zfb-build の値をそのクライアントの識別子と一致させる必要があります。一致しないと、クライアントは ZR_IDENTITY: island <name> has invalid transport identity で島を拒否します。自分で所有するクライアント mount については自分で所有するホストから mount するを参照してください。

関連ページ

Revision History

作成更新