クイックスタート
ブラウザで数を数え、サーバーで 0 に戻すカウンターを、Rust のファイル 1 つで作ります。
依存クレートを追加する
hypersig を axum、serde、tokio と一緒に Cargo.toml に追加してください。
Cargo.toml
[dependencies]
hypersig = "0.2"
axum = "0.8"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }サーバーを書く
src/main.rs を書きます。このファイルにはページ、リセットボタンからの呼び出しに答えるハンドラ、その 2 つを配信するルーターが入ります。
src/main.rs
use axum::Router;
use hypersig::html::{button, div, span};
use hypersig::{Action, Page, Patches, Route, RouterExt, Signals};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Counter {
count: i32,
}
hypersig::routes! {
fn pages(Router<()>);
const INDEX: Route = "/" => index;
}
const RESET: Action<Counter> = Action::post("/reset");
async fn index((): (), (): ()) -> Page<Counter> {
let count = Counter::count();
Page::new("Counter", Counter { count: 0 }).child(
div()
.child(button().on_click(count.set(count + 1)).text("+1"))
.child(span().bind_text(count.to_text()))
.child(button().on_click(RESET.call()).text("Reset on the server")),
)
}
async fn reset((): (), (): ()) -> Patches<Counter> {
Counter::count().patch(0).into()
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let app = pages(Router::new()).hypersig_runtime().action(RESET, reset);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, app).await
}動かす
cargo runhttp://127.0.0.1:3000 を開いてください。+1 ボタンはリクエストを送らずにブラウザで数を増やします。リセットボタンは /reset を呼び出し、ハンドラが返す差分が count を 0 に戻します。ブラウザのランタイムは hypersig_runtime() がクレートから配信するので、Node も JavaScript のビルドも使いません。
各部分の役割
Counterに付けた#[derive(Signals)]は、フィールドを指すSig<Counter, i32>を返すCounter::count()を追加します。フィールド名を打ち間違えるとコンパイルエラーになります。count.set(count + 1)は文です。on_clickはこれを JSON にしてdata-hs-on-click属性に書き込み、ランタイムはクリックのたびにこの文を実行します。span().bind_text(count.to_text())はテキストをカウントの値に合わせ続けます。Page::renderが初期値でこの式を評価するので、HTML の時点で0が表示されています。INDEXは/のページを表すRouteです。routes!がハンドラindexと一緒にこのルートを宣言し、マクロが書く関数pagesがこのページを配信します。このページへのリンクはINDEX.link()です。パラメータを持つルートは ページとリンク で説明します。RESETはAction<Counter>です。ページはRESET.call()で呼び出し、ルーターは.action(RESET, reset)で配信するので、パス、メソッド、引数の型、属するページを両側で共有します。- ハンドラは
Counter::count().patch(0)を返します。サーバーはこれを Server-Sent Event として送り、ランタイムが値をシグナルに書き込むと、テキストが書き換わります。
ここで試す
下のカウンターはこのプログラムのブラウザ側をこのページで動かしています。ボタンには同じリポジトリにあるコンポーネントのクレート hypersig_ui のものを使っています。
実行結果
counter.rs
use hypersig::html::{div, span};
use hypersig::style::Style;
use hypersig::style::pattern::hstack;
use hypersig::style::value::{FontWeight, TextAlign, rem};
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, button};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
pub struct Counter {
count: i32,
}
pub fn page() -> Page<Counter> {
let count = Counter::count();
Page::new(
"カウンター",
Counter { count: 0 },
)
.child(
div()
.css(hstack(rem(1)))
.child(
button(ButtonVariant::Outline, ButtonSize::Default)
.on_click(count.set(count - 1))
.text("-1"),
)
.child(
span()
.css(
Style::new()
.min_width(rem(3))
.text_align(TextAlign::Center)
.font_size(rem(1.5))
.font_weight(FontWeight::Number(600.0)),
)
.bind_text(count.to_text()),
)
.child(
button(ButtonVariant::Default, ButtonSize::Default)
.on_click(count.set(count + 1))
.text("+1"),
),
)
}このサイトは GitHub Pages に置いた静的ファイルなので、デモはシグナルだけで動き、アクションを呼び出すものはありません。
HTML の組み立て方は次の ページを組み立てる で説明します。