ライブラリとして組み込む
Server ビルダー API を介して、Rust ホスト(Tauri・CLI・サービス)から zfb の HTTP サーバーをインプロセスで動かす。
zfb-server クレートは小さなビルダー API を提供しており、Rust ホストが zfb の HTTP サーバーをインプロセスで動かせます。子サイドカーも追加のバイナリも不要です。zfb dev を支えるのと同じクレートが組み込みサーバーを支えており、唯一の違いは誰がライフサイクルを所有するかだけです。
このガイドでは、公開ビルダーの形、リクエスト拡張の注入ポイント、そしてホストハンドラの継ぎ目(with_ssr_handler)を解説します。
Crate availability
zfb-server はまだ crates.io に公開されていないため、docs. リンクは解決されません。workspace crate に git から依存し、タグまたはコミットを pin してください。
[dependencies]
zfb-server = { git = "https://github.com/Takazudo/zudo-front-builder", package = "zfb-server", rev = "<commit>" }どんなときに使うか
ホストが次のことを必要とするとき、ライブラリとして組み込む方法を選びます。
zfb のフルなルートテーブルをデスクトップまたは CLI プロセス内で動かす、
ハンドラが毎リクエストで読むべきプロセス単位のコンテキスト(
AppHandle、FS の権限セット、認証トークンなど)を付与する、特定の URL パターンを、JS ランタイムを一切経由せずに Rust 所有のレスポンス(Markdown ルックアップ、データベース読み込み、サイドカー API ブリッジ)でショートサーキットする。
このモードと子サイドカーを同梱する方法のどちらにするかを決める構成マトリクスについては、Desktop Deployment を参照してください。
ビルダーの形
公開される API 表面は小さいものです。Server 型と ServerBuilder 型を合わせても、公開メソッドは 10 個以下です。
use zfb_server::{Server, ServerHandle, ServerMode, RouteParams};
use axum::http::{Request, StatusCode};
use axum::body::Body;
let server = Server::builder()
.config_path("./zfb.config.json")
.mode(ServerMode::Embed)
.bind("127.0.0.1:0".parse()?)
.with_request_extension(host_ctx.clone())
.with_ssr_handler(
"/api/echo/:msg",
|req: Request<Body>, params: RouteParams| async move {
let msg = params.get("msg").unwrap_or("(none)");
let ctx = req.extensions().get::<HostCtx>().cloned();
(StatusCode::OK, format!("ctx={ctx:?} msg={msg}"))
},
)
.build()?;
let handle: ServerHandle = server.serve_in_thread()?;
println!("listening on {}", handle.addr());
// later:
handle.shutdown()?;ビルダーが確定する主な選択肢:
ServerMode::Embedはライブリロードのスクリプト注入と/SSE エンドポイントをオフにします。それ以外のルートテーブルは_ _ zfb/ reload zfb devと同一です。bind("127.0.0.1:0")は OS にエフェメラルポートを要求します。実際のポートはServerHandle::addr()から読み取ってください。serve_in_thread()は独自のcurrent_threadtokio ランタイムを持つ専用の OS スレッドを起動するため、ホスト側に独自の tokio ランタイムは不要です(Tauri の同期的なsetupコールバックも変更なしで機能します)。ServerHandle::shutdown()は冪等です。2 回呼んでも何も起こりません。
すでに独自の tokio ランタイムを駆動しているホスト向けに、非同期の終端 Server::serve(self, shutdown).await も利用できます。
設定形式: zfb.config.json を優先する
config_path は zfb.config.json と zfb.config.ts の両方を受け付けます。組み込みサーバーでは、zfb.config.json が推奨形式です。ビルドステップなしで直接読み込まれます(このガイドのすべての例もこれを使っています)。
zfb.config.ts もサポートされていますが、TypeScript 設定を評価するには、まず esbuild でバンドルする必要があります。そして組み込みの zfb-server は、zfb CLI とは異なり、自前の esbuild バイナリを同梱していません。そのため、リポジトリ外の組み込み側が .ts ファイルを config_path に指定する場合は、次のいずれかが必要です。
代わりに
zfb.config.jsonを同梱または生成する — esbuild が一切不要で、最もシンプルな道です。build()を呼び出す前に、環境変数ZFB_ESBUILD_BINを利用可能な esbuild CLI バイナリに設定する。
Note
.ts 設定が "esbuild binary not found" エラーで失敗する場合、原因はこれです。zfb.config.json に切り替えるか、ZFB_ESBUILD_BIN を設定してください。
リクエスト拡張の注入
ServerBuilder::with_request_extension::<T>(value) は、受信するすべてのリクエストの http::Extensions マップにクローンされるプロセス単位の値を登録します。ハンドラは req.extensions().get::<T>() でそれを読みます:
#[derive(Clone)]
struct HostCtx { /* … */ }
let server = Server::builder()
.config_path("./zfb.config.json")
.mode(ServerMode::Embed)
.with_request_extension(host_ctx)
.with_ssr_handler("/whoami", |req: Request<Body>, _params| async move {
let ctx = req.extensions().get::<HostCtx>().cloned();
format!("{ctx:?}")
})
.build()?;境界となる T: Clone + Send + Sync + 'static は、リクエスト単位のコンテキスト型に最低限必要なものです。ハンドラが axum::Extension<T> をインポートする必要は一切ありません。そのエクストラクタ型は意図的に zfb-server の公開 API 表面から外されています。
異なる T で with_request_extension を複数回呼ぶと値が蓄積されます。同じ T で 2 回呼ぶと最初のものを上書きします(これは http::Extensions::insert のセマンティクスと一致します)。
ハンドラのシグネチャ
with_ssr_handler は URL パターンと、次の形の非同期関数を受け取ります:
async fn(http::Request<axum::body::Body>, zfb_server::RouteParams) -> impl IntoResponseそれがコントラクトのすべてです。ハンドラは:
受信したリクエストをそのままの
http::Request<Body>として受け取ります。クエリ文字列、ヘッダー、ボディ、拡張のすべてがリクエスト自体からアクセス可能です。キャプチャされたルートパラメータを、
params.get("name")でルックアップできるRouteParams値として受け取ります。axum::response::IntoResponseを実装する任意のもの(String、タプル(StatusCode, String)、完全なhttp::Response<…>、カスタム型など)を返します。
このシグネチャが意図的に HTTP 的な形になっているのは、ハンドラが何のためのものかに言及しないためです。同じプリミティブが Markdown ルックアップ、データベース読み込み、IPC ブリッジ、あるいはその他のリクエスト時レスポンスを支えられます。サーバーが見るのはバイトの入出力だけです。
ルートパターンの文法
パターンは先頭スラッシュ付きのパスです。各セグメントは次のいずれかです:
| 形式 | マッチ対象 | キャプチャ先 |
|---|---|---|
foo | リテラルなセグメント foo | — |
:name | ちょうど 1 つの空でないセグメント | params.get("name") |
*name | 残りの 1 つ以上のセグメントを / で連結したもの | params.get("name") |
*name ワイルドカードはパターンの最終セグメントでなければなりません。空のキャプチャ(:name スロットに対する素の /)は拒否されます。
例:
/はhealth /のみにマッチします。health /はusers/ : id /にマッチし、users/ 42 id = "42"となります。/はfiles/ *rest /にマッチし、files/ a/ b. txt rest =となります。"a/ b. txt" /は両方を組み合わせています。projects/ : proj/ refs/ *rest
優先順位: ホストハンドラがランタイム SSR に勝つ
これは組み込みの継ぎ目における最も重要なルールです。開発ルーターがリクエストを受け取ると、次の順序で各レイヤーを試します:
プラグインの dev-middleware(最長プレフィックスマッチ)— 登録されたプラグインがそのパスを要求する場合、
ホストが登録した Rust ハンドラ(このビルダーメソッド)— 上記のパターンのいずれかがマッチする場合、
リクエスト時 SSR —
SsrRouteSetがそのパスを要求する場合、メモリ内ページキャッシュ(SSG 出力)、
dist/へのディスク上フォールバック、public/へのディスク上フォールバック、dev の 404。
ホストハンドラは、同じ URL を要求するランタイム SSR ページよりも常に優先されます。プロジェクトに / の Rust ハンドラと、同じく / を配信する prerender = false ページの両方がある場合、Rust ハンドラが勝ち、SSR ディスパッチャは一切呼び出されません。
この方向にする理由:
ホストはプロセスの信頼された所有者です。あるパスにハンドラを登録するなら、それは意図的なオーバーライドです。
フォールスルーが必要なハンドラは、登録しないことを選べます。優先順位は登録時に決まるのであって、ハンドラがセンチネルを返すことによってではありません。
逆順にすると、ランタイムページをオーバーライドしたいホストはすべて、ページのエクスポートをビルド時フラグの後ろにゲートする必要が生じます。これは、組み込みの継ぎ目が意図的に分離している 2 つの関心事を結合させてしまいます。
ライフサイクルとシャットダウン
Server::serve_in_thread() は ServerHandle を返します。このハンドルは Clone なので、ホストは複数のシャットダウン呼び出し箇所(Tauri の on_window_event、Ctrl-C ハンドラ、HTTP の / ルート)にコピーを渡せます。すべてのクローンは Arc<Mutex<…>> の背後で同じワンショットのセンダーと join ハンドルを共有します:
let handle = server.serve_in_thread()?;
let h2 = handle.clone();
ctrl_c_callback(move || {
let _ = h2.shutdown(); // idempotent
});
// On the main thread, wait for the server to exit:
handle.join()??;shutdown() はグレースフルシャットダウンのシグナルを一度だけ送ります。それ以降の呼び出しは何もしません。join() はシングルショットで、スレッドを待てるのは 1 つの呼び出し元だけです。
ライブコンテンツ / ファイル監視
ここまでで説明したのは、静的 dist サーバーです。build() が zfb.config.json を読み、ルートテーブルは dist/ 内のファイルへフォールスルーします。これは、コンテンツがビルド時に固定されるパッケージ済みアプリに適した形です。
一方で、ホストが実行時にサーバーへコンテンツを押し込みたい場合があります。たとえば、ファイル監視のコールバックから書き込む PageCache や、再ビルドごとのライブリロードシグナルです。その場合は、必要な制御の度合いに応じて 2 つのレベルを選べます。
レベル 1: ビルダー経由で PageCache を投入する
ServerBuilder::with_page_cache(cache) は、build() がデフォルトで割り当てる空のものではなく、あなたが所有する PageCache をサーバーに渡します。PageCache は Clone です(内部的には Arc<RwLock<…>>)。そのため、手元にハンドルを保持し、クローンをビルダーに渡してから、実行時に同じ共有マップへ書き込めます。稼働中のサーバーはすべての insert を見ます。
use zfb_server::{Server, ServerMode, PageCache};
let cache = PageCache::new();
cache.insert("/", "<h1>initial</h1>").await; // seed before serving
let server = Server::builder()
.config_path("./zfb.config.json")
.mode(ServerMode::Embed)
.with_page_cache(cache.clone())
.build()?;
let handle = server.serve_in_thread()?;
// later — from your own file-watcher callback or rebuild loop:
cache.insert("/blog/foo", rebuilt_html).await; // add or replace one URL
cache.remove(["/blog/stale"]).await; // drop a vanished routeキャッシュキーは、ブラウザが要求する先頭スラッシュ付きの URL パスです(/、/、/)。ページキャッシュのレイヤーは、上の優先順位リストでディスク上の dist/ フォールバックより上にあるため、キャッシュヒットは同じ URL の静的ファイルを覆い、ミスした場合はそのままディスクへフォールスルーします。PageCache は、HTML 以外や一括再ビルドの用途向けに、insert_with_content_type、insert_bytes_with_content_type(バイナリボディ)、replace_all(アトミックな全置換)も公開しています。
必要なのが動的なページバイトだけなら、これで十分です。これが提供しないのはライブリロードチャンネルです。ServerMode::Embed はライブリロードスクリプトを注入せず、/ SSE エンドポイントもマウントしません。また、ビルダーは broadcast センダーを内部で所有します。
レベル 2: ServeOpts + serve_with_listener へ降りる
ページキャッシュ以上のものが必要な場合、たとえば稼働中のライブリロード broadcast チャンネル、リクエスト時 SSR ルート(SsrRoutesHandle)、プラグイン dev-middleware が必要な場合、ビルダーは意図的に入口ではありません。自分で ServeOpts を構築し、低レベルの serve_with_listener(opts, listener, shutdown)(またはリスナーもバインドする serve(opts, shutdown))を呼び出します。どちらも公開され安定しています。zfb dev 自身が使っているのと同じ入口です。
ServeOpts は直接フィールドを埋めるプレーンな構造体です。ライブコンテンツに関係するフィールドは次のとおりです。
pages: PageCache— レベル 1 と同じ共有キャッシュです。監視側からこれに書き込みます。broadcast: ReloadTx— ライブリロードのセンダーです。ReloadTxはtokio::sync::broadcast::Sender<ReloadEvent>です。再ビルドごとにReloadEventを送ると、SSE ストリームを受信している接続済みクライアントがリロードします。(tokio::sync::broadcast::channel::<ReloadEvent>(capacity)でチャンネルを作り、Senderを保持します。選んだmodeによって SSE エンドポイントがマウントされるかが決まります。)ssr_routes: Option<SsrRoutesHandle>—prerender = falseページ向けのリクエスト時 JS SSR で、セッション中に差し替え可能です。mode: ServerMode— ライブリロードのスクリプト注入と/SSE エンドポイントのマウントが必要なら_ _ zfb/ reload ServerMode::Devを選びます。Embed/Previewではこれらはオフです。
use zfb_server::{serve_with_listener, ServeOpts, ServerMode, PageCache, ReloadEvent};
use tokio::net::TcpListener;
let pages = PageCache::new();
let (broadcast, _rx) = tokio::sync::broadcast::channel::<ReloadEvent>(64);
let opts = ServeOpts {
project_root: project_root.clone(),
dist_root: dist_root.clone(),
dev_assets_root: None,
html_root: dist_root.clone(), // embed/preview callers alias dist_root
public_root,
addr: "127.0.0.1:0".parse()?,
pages: pages.clone(),
broadcast: broadcast.clone(),
plugins: None,
injected_routes: None,
ssr_routes: None,
base: None,
trailing_slash: false,
mode: ServerMode::Dev, // Dev mounts the livereload SSE endpoint
islands_bundle_url: None,
css_bundle_url: None,
allowed_hosts: Vec::new(),
bound_host: None,
render_on_request_hook: None,
};
let listener = TcpListener::bind(opts.addr).await?;
// from your watcher: pages.insert(url, html).await; then broadcast.send(reload_event);
serve_with_listener(opts, listener, std::future::pending()).await?;ServeOpts 内のすべてのパスは絶対パスである必要があり、自分で解決します(ビルダーの config_path ローダーはこれを代行します。レベル 1 で足りる場合にそちらを優先する理由の 1 つです)。これは最も低い公開レイヤーです。zfb dev の bin クレートが、ファイル監視と再ビルドのオーケストレーターを差し込んでいる場所そのものです。
MDX 監視再ビルドの全体
キャッシュ投入は動的なバイト列の配信を扱いますが、zfb の MDX コンパイル / island バンドルパイプラインを実行してくれるわけではありません。それは zfb bin クレート側にあります。ユーザーがローカルで Markdown を編集し、完全に再レンダーされたプレビューを見る必要がある場合は、Desktop Deployment の「より難しいケース」の注記が、そのフルパイプライン向けに zfb dev をサイドカー(Mode B)として起動する方法を扱っています。
メソッド予算
組み込みの API 表面は意図的に小さく保たれています。Server 型と ServerBuilder 型を合わせて公開するメソッドは 9 個です。Server::builder、Server::serve、Server::serve_in_thread、それに 6 つのビルダーメソッド(config_path、mode、bind、with_request_extension、with_ssr_handler、build)です。ServerHandle::addr、ServerHandle::shutdown、ServerHandle::join は意図的に別の型に置かれており、この予算からは除外されています。
with_page_cache(上記)はライブコンテンツ向けの脱出口であり、これも設計上このカウントから除外されています。これは ServeOpts へ降りることに対応するビルダーレベルの手段であって、静的 dist 向けの中核 API 表面の一部ではありません。
将来の機能で合計 10 個を超える必要が生じた場合、正しい対応は継ぎ目を再設計するフォローアップイシューであって、10 個目のメソッドを付け足すことではありません。
サンプルクレート
最小限のエンドツーエンドの例が crates/ にあります。ワークスペースのルートからビルドしてください:
cargo build --manifest-path crates/zfb-server/examples/embed/Cargo.tomlあるいは実行します(この例はサーバーを起動し、ハンドラが注入されたコンテキストとともに応答することを示すために HTTP リクエストを 1 回発行し、シャットダウンします):
cargo run --manifest-path crates/zfb-server/examples/embed/Cargo.tomlこのサンプルクレートは空の [workspace] テーブルを使って親ワークスペースから自身を除外しているため、ルートのマニフェストがこれをメンバーとして列挙する必要はありません。