Mi az a Leptos és miért érdemes full-stack Rust webfejlesztéshez

Ha eddig React vagy Vue felől érkeztél a Rust világába, a Leptos pont azt az érzést próbálja megadni, csak Rustban: reaktív, komponens-alapú UI-t, ami ráadásul szerveroldalon (SSR) és böngészőben (WASM) is lefut ugyanabból a kódbázisból. A Leptos 0.5 (2023 október elején jelent meg) egy komoly mérföldkő volt a projekt életében – letisztultabb API, jobb hydration-teljesítmény, és a cargo-leptos build eszköz is jóval kiforrottabbá vált.

A full-stack megközelítés lényege, hogy a komponenseidet és a signalokat egyszer írod meg, és a Leptos gondoskodik arról, hogy szerveren renderelje ki a kezdeti HTML-t (SSR), majd a kliens oldalon WASM-mel "hydrate"-elje, azaz interaktívvá tegye ugyanazt a fát. Ez azt jelenti, hogy nincs külön backend API meg külön frontend JS projekt – mindkettő egy Cargo workspace-ben él.

Tipp

Ha korábban Yew-val dolgoztál, a Leptos signal-alapú reaktivitása sokkal közelebb áll a SolidJS-hez, mint a virtual DOM-os megközelítéshez. Ez gyakran gyorsabb újrarenderelést eredményez, mert nem kell diffelni egy fát.

Projekt inicializálása cargo-leptos segítségével

Először telepítsd a cargo-leptos eszközt, ami a build folyamatot (WASM fordítás, SSR bináris, asset kezelés) orchestrálja:

cargo install cargo-leptos

Ezután hozz létre egy új projektet a beépített sablonnal:

cargo leptos new --git leptos-rs/start-axum leptos-todo
cd leptos-todo

Ez egy Axum-alapú SSR szervert állít be alapból (a Leptos jól integrálódik Axum-mal, de Actix-web is támogatott alternatíva). A generált Cargo.toml-ban valami ilyesmit fogsz találni:

[package]
name = "leptos-todo"
version = "0.1.0"
edition = "2021"

[dependencies]
leptos = { version = "0.5", features = ["csr", "ssr"] }
leptos_axum = { version = "0.5" }
leptos_meta = { version = "0.5", features = ["ssr"] }
leptos_router = { version = "0.5", features = ["ssr"] }
axum = { version = "0.6", optional = true }
tokio = { version = "1", features = ["full"], optional = true }

[features]
ssr = [
  "leptos/ssr",
  "leptos_axum",
  "dep:axum",
  "dep:tokio",
]
csr = ["leptos/csr"]

[package.metadata.leptos]
site-root = "target/site"
site-pkg-dir = "pkg"
output-name = "leptos-todo"

A cargo leptos watch paranccsal fejlesztés közben automatikus rebuild-et és hot reload-ot kapsz mind a szerver, mind a WASM oldalon.

Megjegyzés

A cargo-leptos a háttérben kétszer fordítja a kódot: egyszer natívan (SSR bináris) és egyszer wasm32-unknown-unknown target-tel (kliensoldali hydration). Ezért érdemes SSD-t és türelmet hozni az első build-hez.

Reaktív komponensek és signalok bemutatása

A Leptos reaktivitásának alapköve a Signal (pontosabban RwSignal, ReadSignal és WriteSignal). A create_signal függvény egy pár getter/setter-t ad vissza, amit egy Scope-hoz (cx) kötsz.

use leptos::*;

#[component]
fn Counter(cx: Scope) -> impl IntoView {
    let (count, set_count) = create_signal(cx, 0);

    view! { cx,
        <button on:click=move |_| set_count.update(|n| *n += 1)>
            "Számláló: " {count}
        </button>
    }
}

Az on:click egy natív DOM event handler, a {count} pedig egy reaktív "lyuk" a view-ban, ami automatikusan frissül, amikor a signal értéke változik – anélkül, hogy a teljes komponenst újrarenderelnéd. Ez a finomszemcsés reaktivitás a Leptos egyik legnagyobb erőssége a virtual DOM-os megoldásokkal szemben.

Jó tudni

A Scope (cx) paraméter minden komponensnek és sok reaktív primitívnek szükséges, mert ez tárolja a komponensfa élettartamához kötött reaktív gráfot. Ne próbálj signalokat scope nélkül létrehozni – ownership és lifetime szempontból ez a Leptos backbone-ja.

Szerveroldali renderelés (SSR) beállítása

Az SSR-hez a leptos_axum crate ad segédfüggvényeket, amik becsomagolják a Leptos view-t egy Axum route handler-be. A main.rs-ben valahogy így néz ki a szerver felállítása:

#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
    use axum::Router;
    use leptos::*;
    use leptos_axum::{generate_route_list, LeptosRoutes};

    let conf = get_configuration(None).await.unwrap();
    let leptos_options = conf.leptos_options;
    let addr = leptos_options.site_addr;
    let routes = generate_route_list(App);

    let app = Router::new()
        .leptos_routes(&leptos_options, routes, App)
        .fallback(leptos_axum::file_and_error_handler)
        .with_state(leptos_options);

    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
    axum::serve(listener, app.into_make_service())
        .await
        .unwrap();
}

Az App a gyökér komponensünk, amit a leptos_meta (cím, meta tag-ek kezelése) és a leptos_router (kliensoldali navigáció) is felhasznál. A lényeg, hogy a generate_route_list bejárja a route definícióidat, és minden ismert útvonalra beállít egy SSR handler-t – nincs kézzel írt boilerplate minden oldalhoz.

Egy egyszerű TODO alkalmazás megépítése

Nézzük meg a teljes kört egy minimális TODO alkalmazáson. Első lépés az adatmodell és a signal, ami a listát tárolja:

use leptos::*;

#[derive(Clone, Debug, PartialEq)]
struct Todo {
    id: usize,
    text: String,
    done: bool,
}

#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
    let (todos, set_todos) = create_signal::<Vec<Todo>>(cx, vec![]);
    let (input, set_input) = create_signal(cx, String::new());
    let mut next_id = 0usize;

    let add_todo = move |_| {
        let text = input.get();
        if text.trim().is_empty() {
            return;
        }
        set_todos.update(|list| {
            list.push(Todo {
                id: next_id,
                text: text.clone(),
                done: false,
            });
        });
        next_id += 1;
        set_input.set(String::new());
    };

    view! { cx,
        <div class="todo-app">
            <h2>"TODO lista"</h2>
            <input
                type="text"
                prop:value=input
                on:input=move |ev| set_input.set(event_target_value(&ev))
            />
            <button on:click=add_todo>"Hozzáadás"</button>
            <ul>
                <For
                    each=todos
                    key=|todo| todo.id
                    view=move |cx, todo: Todo| {
                        view! { cx,
                            <li>
                                <input
                                    type="checkbox"
                                    checked=todo.done
                                    on:change=move |_| {
                                        set_todos.update(|list| {
                                            if let Some(t) = list.iter_mut().find(|t| t.id == todo.id) {
                                                t.done = !t.done;
                                            }
                                        });
                                    }
                                />
                                <span>{todo.text.clone()}</span>
                            </li>
                        }
                    }
                />
            </ul>
        </div>
    }
}

Ebben a komponensben van pár fontos elem, amit érdemes megérteni:

  • A <For> komponens felel a lista finomszemcsés rendereléséért. A key closure adja meg, hogy melyik elem melyik DOM node-hoz tartozik – ez elengedhetetlen, hogy a Leptos ne renderelje újra a teljes listát minden módosításnál.
  • A prop:value attribútum a natív DOM property-t köti a signalhoz, nem csak a HTML attribútumot – így a controlled input helyesen frissül.
  • Az event_target_value egy segédfüggvény, ami kényelmesen kiolvassa az input mezőből a string értéket az eseményből.
Figyelem

A next_id egy sima usize változó, amit a closure mutál – ez működik, mert a closure move és a komponens csak egyszer fut le a saját scope-jában, de bonyolultabb állapotkezelésnél inkább tegyél mindent signalba, különben könnyen inkonzisztens állapotot kapsz SSR és hydration között.

Ha ezt a komponenst beillesztjük az App gyökérbe, és lefuttatjuk a cargo leptos watch parancsot, a böngészőben azonnal látjuk a listát, és minden interakció (hozzáadás, checkbox váltás) szerver-oldali renderelés nélkül, tisztán a WASM oldali reaktív gráfban történik – miközben az első betöltéskor a szerver már kész HTML-t küldött, tehát a SEO és az első betöltési sebesség is rendben van.

Összefoglalás

A Leptos 0.5 egy nagyon érett pontra ért a full-stack Rust webfejlesztésben: a cargo-leptos eszköz kényelmes fejlesztői workflow-t ad, a signal-alapú reaktivitás gyors és intuitív, az SSR pedig Axum-mal (vagy Actix-web-bel) kézenfekvő módon köthető be. A bemutatott TODO alkalmazás persze csak a jéghegy csúcsa – innen már csak egy lépés a szerveroldali perzisztencia (adatbázis), a create_resource async adatlekérés, vagy a leptos_router segítségével több oldalas navigáció bevezetése. Ha ownership és lifetime szempontból már otthonosan mozogsz Rustban, a Leptos tanulási görbéje meglepően rövid – érdemes egy hétvégét rászánni.