English

クイックスタート

ブラウザで数を数え、サーバーで 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 run

http://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 の組み立て方は次の ページを組み立てる で説明します。