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.
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.
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.
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. Akeyclosure 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:valueattribú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_valueegy segédfüggvény, ami kényelmesen kiolvassa az input mezőből a string értéket az eseményből.
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.