デスクトップ展開
zfb でビルドしたサイトを Tauri・Electron などのデスクトップアプリケーションに組み込む方法。
このガイドでは、zfb サイトをデスクトップアプリに組み込みたい場合に、現時点で現実的に何ができるかを解説します。zfb と Tauri を組み合わせる構成には 4 つの異なるモードがあり、それぞれ異なるプロジェクトに対する正解です。意図を持って選択してください。
モード A は、コンテンツがビルド時に作成され、各リリースに同梱される場合に最適です。
モード B は、Tauri 側のコードを最小限に抑えつつ zfb のランタイムの動的機能をフルに使いたい場合に最適です。
モード C は、アプリの動的な部分を、zfb をプロセスに含めずに Rust で実現できる場合に最適です。
モード D は、子プロセスもポート管理もない緊密なインプロセス Tauri 統合を求める場合に最適です。
4 つのモードすべてを支配する中核的な不変条件:
ビルド時 = Node.js が必須。ランタイム = Node.js は一切不要。
zfb の静的な出力は意図的にランタイム非依存に設計されています。一方、ビルドツールはそうではありません。この境界に逆らうのではなく、この境界を前提にアーキテクチャを設計してください。
モード A — dist/ のみを同梱する
概要。 zfb build は dist/ に完全に静的なサイト(HTML・CSS・JavaScript ファイルで、ランタイムにサーバーサイドの依存を持たない)を生成します。デスクトップアプリは、フレームワーク組み込みのアセットサーバーを使ってそのディレクトリを直接配信できます。zfb はランタイムでは見えず、パッケージされたアプリはディスク上のただのファイルです。
選ぶべきとき。 ドキュメントアプリ、ヘルプシステム、そしてコンテンツがビルド時に作成されリリースに同梱されるあらゆるデスクトップアプリ。ユーザーが実行中のアプリ内でコンテンツを編集する必要がないなら、モード A が正解です。
必要な作業。 Tauri の場合、関連する tauri.conf.json のキーは次のとおりです:
{
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:3000"
}
}Note
Tauri v2 では distDir → frontendDist、devPath → devUrl にキー名が変更されました。 Tauri v1 を使っている場合は旧来のキー名を使い、代わりにTauri v1 設定リファレンスを参照してください。
frontendDist を dist/ フォルダに向けます(相対パスはプロジェクト構成に合わせて調整してください)。残りは Tauri 組み込みの静的ファイルサーバーが処理します。パッケージされたアプリに Node.js プロセスや HTTP サーバーは不要です。
開発マシン(または CI)で
zfb buildを実行する。生成された
dist/を Tauri アプリのバンドルに含める。出荷する。完了です。
アセット配信のオプション一式(カスタムプロトコルやセキュリティポリシーを含む)については、Tauri v2 設定リファレンスを参照してください。
トレードオフ。 コンテンツの更新には新しいアプリのリリースが必要です。新しいビルドなしにユーザーが最新のコンテンツを見られる仕組みはありません。
モード B — zfb バイナリを Tauri のサイドカーとして動かす
概要。 Tauri が zfb バイナリを子プロセスとして起動し、WebView は localhost:<port> を指します。zfb はその子プロセスの中で、リクエスト時の MDX レンダリングやリソース探索などを処理します。サイドカーのバイナリは Rust であり、パッケージされたアプリに Node.js は不要です。
選ぶべきとき。 zfb の動的機能をフルに使いたいが(prerender = false のルート、リクエスト時の MDX レンダリング、リビルドなしのコンテンツ再読み込み)、zfb サーバーを Tauri プロセス自体の中に置く必要はない、という場合。
必要な作業。 Tauri サイドカーとして zfb バイナリを Tauri アプリと並べて同梱します。Tauri フロントエンドがサイドカーを起動し、ポートを発見し、WebView を http: に開けるよう、薄い IPC ブリッジを書きます。サイドカーと Tauri の境界はクリーンですが、統合のためのつなぎ込みは標準では提供されません。
トレードオフ。 追加で可動部品を抱えることになります(子プロセスのライフサイクル管理、ポート発見、Tauri とサイドカー間の IPC 配線)。その代わり、Node.js 依存なしで zfb のフルなレンダリングパイプラインをランタイムで利用できます。
モード C — zfb をビルド時のみ使うカスタム Rust クレート
概要。 zfb はビルド時に一度だけ Preact のシェルを dist/ にコンパイルします。それ以降のランタイムの動的機能(リソース探索、Markdown レンダリング、配信)はすべて手書きの Rust コードであり、典型的にはディスクからファイルを読み込み、事前ビルドしたシェルにコンテンツを差し込む Axum サーバーです。zfb はランタイムには一切存在せず、純粋にビルド時のツールです。
選ぶべきとき。 アプリの動的な部分が Markdown レンダリングか、あるいは素直な Rust ライブラリですでにうまく扱える何かであり、zfb のフルなレンダリングパイプラインをバイナリに持ち込むより、焦点を絞った Rust サーバーコードを書きたい場合。可能な限り小さいランタイムフットプリントを求めている場合です。
必要な作業。 zfb build で Preact のシェルをビルドします。続いて、リクエスト時に Markdown ファイルを読み込み、Rust の Markdown クレートでレンダリングし、その結果をセンチネル置換などのパターンで静的シェルに差し込む、専用の Rust サーバー(または既存の Tauri アプリへの Axum ルートの追加)を書きます。CCResDoc はこのパターンの具体的で動作する実例です。zfb が一度だけ Preact のシェルをコンパイルし、手書きの Rust バックエンドがランタイムにコンテンツを配信します。
トレードオフ。 より多くの Rust のつなぎコードを書くことになり、ランタイムでの TSX レベルのページコンポーネントを諦めることになります。その代わり、可能な限り小さいバイナリ、ランタイム動作の完全な制御、そしてパッケージされたアプリでの V8 依存ゼロを得られます。
モード D — zfb を Rust クレートとして Tauri 内に埋め込む
概要。 Tauri の setup フックが、Tauri プロセス内の専用 OS スレッド上で zfb サーバーを起動します。子プロセスもポート管理もなく、prerender = false のルートや Tauri IPC 呼び出しを Rust 対 Rust のインプロセス受け渡しで処理します。WebView は、埋め込まれたサーバーがバインドするエフェメラルなポートを指します。
選ぶべきとき。 zfb のランタイムの動的機能と緊密な Tauri 統合(IPC、ファイルシステムアクセス、コンテンツのホットリロード)を、モード B のサイドカーのライフサイクル配線なしに使いたい場合。
必要な作業。 zfb-server を Cargo の依存として追加し、Tauri の同期的な setup コールバックの中で Server::builder() を呼び出します:
use zfb_server::{Server, ServerMode};
tauri::Builder::default()
.setup(|app| {
let handle = Server::builder()
.config_path("./zfb.config.json")
.mode(ServerMode::Embed)
.bind("127.0.0.1:0".parse()?)
.with_request_extension(app.handle().clone())
.build()?
.serve_in_thread()?;
let port = handle.addr().port();
// open the WebView at http://127.0.0.1:{port}
Ok(())
})serve_in_thread() は自身の current_thread tokio ランタイムを持つ専用の OS スレッドを起動するため、Tauri の同期的な setup コールバックは変更なしで機能します。WebView を開く前に、バインドされたポートを ServerHandle::addr() から読み取ってください。
ビルダー API の全体、リクエスト拡張の注入、そしてホストハンドラの継ぎ目については、Embed as Library ガイドを参照してください。
トレードオフ。 サイドカーの配線なし、より緊密な IPC、よりクリーンなパッケージング。埋め込みビルダー API は意図的に小さく保たれているため(Server + ServerBuilder で 10 メソッド以下)、複雑なリクエストごとのルーティングは任意の axum ミドルウェアではなく、引き続き with_ssr_handler のパターンを通します。
より難しいケース: アプリ内でコンテンツをリビルドする
ユーザーがローカルで Markdown ファイルを編集し、実行中のアプリ内でライブプレビューを見られるようにする必要がある場合(要するにパッケージされたウィンドウ内で zfb dev を動かす場合)、正しいアプローチは必要なものによって変わります。
フルな MDX レンダリングが必要なら、モード B(サイドカー)が今日の最善の道です。サイドカーがファイル監視 / インクリメンタルリビルドのループをバックグラウンドで動かし続け、WebView はアプリの再起動なしに変更を反映します。
モード D(インプロセス埋め込み)は、緊密な統合のための望ましい道です。子プロセスもポートもなく、
with_request_extensionを通じて Tauri IPC とファイルシステムに直接アクセスできます。モード A や C はここでは役に立ちません。どちらもパッケージされたアプリが動く時点でコンテンツが静的であることを前提にしているためです。
ランタイムで Node.js が必要な場合(たとえば Node.js 依存を持つカスタム zfb プラグインを動かす場合)は、代わりに Electron を検討してください。Electron は Node.js を埋め込むため、zfb dev はメインプロセス内で自然に動作します。その代償として、バイナリサイズが大幅に増加し、パッケージングがより複雑になります。これは zfb のモードそのものではなく、ランタイムでの Node.js 要件によって決まるフレームワークの選択です。
Electron・Wails などのデスクトップフレームワークはどうか
どのデスクトップフレームワークを使っても、話は同じです。
Electron —
dist/は静的アセットディレクトリとして機能します(loadFile()かfile:プロトコルハンドラを使用)。Electron は Node.js を埋め込むためメインプロセス内で/ / zfb devを動かすことは可能ですが、それは zfb のビルドツールチェーン一式をアプリと一緒に同梱することを意味します。Wails — WebView を埋め込み、埋め込み Go サーバーからアセットを配信します。
dist/ディレクトリに向けてください。ビルド時の Node.js 要件は上記と同じです。Neutralino・Tauri、その他組み込みの静的ファイルサーバーを持つフレームワーク —
dist/を同梱すれば、Node へのランタイム依存はありません。
dist/ の出力はただのファイルです。ディスクからファイルを配信できるフレームワークなら、どれでも機能します。
Tauri 固有のヒント
Tauri 統合についての実践的なメモをいくつか。
アセットソースとしての dist/。 frontendDist(Tauri v2)または distDir(Tauri v1)を zfb が生成する dist/ ディレクトリに向けて設定します。開発中は devUrl(Tauri v2)または devPath(Tauri v1)を http:(デフォルトの zfb dev ポート)に設定し、古いスナップショットではなくライブの開発サーバーから Tauri がロードするようにします。
コンテンツセキュリティポリシー(CSP)。 Tauri のデフォルト CSP はインラインスクリプトをブロックすることがあります。zfb のアイランドハイドレーションはインラインの <script type="module"> タグを使用します。アプリでアイランドを使う場合は、tauri.conf.json の CSP ポリシーを緩和または拡張してください。
ファイルパス。 zfb build はデフォルトで絶対ルート相対のパス(/)を生成します。tauri: プロトコルを使う場合、Tauri のカスタムプロトコルがこれらを正しく書き換えます。file: を直接使う場合は、zfb.config.ts の base を相対パスに設定する必要があるかもしれません。
関連項目
Embed as Library — モード D 向けの
Serverビルダー API の全体、リクエスト拡張の注入、そしてホストハンドラの継ぎ目。Build pipeline —
zfb buildがどのようにdist/を生成し、各クレートが出力に何を寄与するか。Architecture: build engine — クレートの分割と、
dist/への書き込みがアトミックである理由。Installation — ビルドおよび開発ツールの Node.js 要件はここに記載されています。