Leptos: Rust 全栈 Web 框架完全指南¶
为什么要学 Leptos¶
Rust 的性能与安全性带到 Web 开发:Leptos 编译为 WebAssembly(Wasm),在浏览器中运行速度接近原生。Rust 的所有权系统和类型系统在编译时消除了空指针、数据竞争等整类 bug,Web 应用也能享受这些安全保障。
细粒度响应式,无虚拟 DOM:Leptos 采用与 SolidJS 类似的细粒度响应式系统。数据变化时只更新受影响的 DOM 节点,不做整棵树的 diff。在 JS Framework Benchmark 中,Leptos 性能在所有 Web 框架中名列前茅。
真正的全栈 Rust:前端(Wasm)和后端(Actix-web/Axum)共享同一份 Rust 代码。Server Functions 让你在组件中直接调用服务端逻辑,无需手动写 API。类型安全贯穿前后端。
多种渲染模式:支持 CSR(客户端渲染)、SSR(服务端渲染)、Islands(岛屿架构)以及静态站点生成。可以根据应用需求灵活选择,甚至混合使用。
Rust 生态整合:可以直接使用 Rust 生态中的海量 crate——serde 序列化、tokio 异步运行时、sqlx 数据库访问、tracing 日志等,不需要在 JS 和原生之间架桥。
核心概念详解¶
Leptos 是什么(白话解释)¶
如果你了解 React 或 SolidJS,可以把 Leptos 理解为"Rust 版的 SolidJS"。它让你用 Rust 写前端组件,编译成 WebAssembly 在浏览器运行。同时,它也能在服务端渲染 HTML,实现 SSR。
核心理念是"信号"(Signal):你创建一个信号,它就像一个可以被监听的变量。当信号的值改变时,所有使用它的地方自动更新,不需要手动通知,也不需要虚拟 DOM。
信号系统(Signals)¶
use leptos::prelude::*;
#[component]
fn Counter() -> impl IntoView {
// 创建信号:返回 (getter, setter)
let (count, set_count) = signal(0);
// 派生信号:自动追踪依赖
let doubled = move || count.get() * 2;
let is_even = move || count.get() % 2 == 0;
view! {
<div>
<p>"计数: " {count}</p>
<p>"双倍: " {doubled}</p>
<p>{move || if is_even() { "偶数" } else { "奇数" }}</p>
<button on:click=move |_| set_count.update(|n| *n += 1)>
"+1"
</button>
<button on:click=move |_| set_count.set(0)>
"重置"
</button>
</div>
}
}
信号类型对比¶
| 类型 | 用途 | 示例 |
|---|---|---|
signal(value) | 可读写的响应式值 | let (name, set_name) = signal("张三".to_string()); |
move \|\| expr | 派生计算(闭包) | let doubled = move \|\| count.get() * 2; |
Memo::new(fn) | 缓存的派生计算 | let expensive = Memo::new(move \|_\| heavy_calc(count.get())); |
Effect::new(fn) | 副作用 | Effect::new(move \|_\| log!("{}", count.get())); |
Resource::new(fn, fetcher) | 异步数据获取 | 从服务器获取数据 |
RwSignal::new(value) | 读写合一的信号 | let count = RwSignal::new(0); |
组件与视图宏¶
use leptos::prelude::*;
// 组件是一个函数,用 #[component] 宏标记
#[component]
fn UserCard(
// Props 通过函数参数定义
name: String,
#[prop(default = 0)] age: u32,
#[prop(optional)] email: Option<String>,
#[prop(into)] class: String, // into 自动转换类型
) -> impl IntoView {
view! {
<div class=class>
<h2>{name}</h2>
<p>"年龄: " {age}</p>
{email.map(|e| view! { <p>"邮箱: " {e}</p> })}
</div>
}
}
// 使用组件
#[component]
fn App() -> impl IntoView {
view! {
<UserCard
name="张三".to_string()
age=25
email=Some("zhang@example.com".to_string())
class="user-card"
/>
}
}
控制流¶
#[component]
fn ControlFlow() -> impl IntoView {
let (count, set_count) = signal(0);
let (items, set_items) = signal(vec!["苹果", "香蕉", "橙子"]);
let (show, set_show) = signal(true);
view! {
// 条件渲染
<Show
when=move || show.get()
fallback=|| view! { <p>"已隐藏"</p> }
>
<p>"可见内容"</p>
</Show>
// 列表渲染(静态列表)
<For
each=move || items.get()
key=|item| item.to_string()
children=|item| view! { <li>{item}</li> }
/>
// 动态类名
<div class:active=move || count.get() > 5>
"动态样式"
</div>
// 动态属性
<input
type="text"
prop:value=move || count.get().to_string()
on:input=move |ev| {
let val = event_target_value(&ev);
if let Ok(n) = val.parse::<i32>() {
set_count.set(n);
}
}
/>
}
}
Leptos vs Yew vs Dioxus 对比¶
| 特性 | Leptos | Yew | Dioxus |
|---|---|---|---|
| 响应式 | 细粒度信号 | 虚拟DOM(类React) | 虚拟DOM(类React) |
| SSR | 完整支持 | 实验性 | 支持(LiveView模式) |
| 全栈 | Server Functions | 需手动API | Server Functions |
| 路由 | 内置 | yew-router | 内置 |
| 性能 | 极高(无VDOM) | 高 | 高 |
| 语法风格 | 类SolidJS | 类React(hooks风格) | 类React(RSX宏) |
| 跨平台 | Web专注 | Web专注 | Web/桌面/移动 |
| 成熟度 | 活跃开发 | 较成熟 | 活跃开发 |
| 社区 | 快速增长 | 最大Rust前端社区 | 增长中 |
| Islands | 支持 | 不支持 | 不支持 |
| 学习曲线 | 中(需了解信号) | 中(类React) | 中低(类React) |
安装与配置¶
环境准备¶
# 1. 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. 添加 Wasm 编译目标
rustup target add wasm32-unknown-unknown
# 3. 安装 cargo-leptos(构建工具)
cargo install cargo-leptos
# 4. 安装 Trunk(可选,用于纯 CSR 项目)
cargo install trunk
创建新项目(SSR 全栈)¶
或手动创建:
Cargo.toml:
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "0.7", features = ["nightly"] }
leptos_meta = { version = "0.7" }
leptos_router = { version = "0.7" }
leptos_axum = { version = "0.7", optional = true }
axum = { version = "0.7", optional = true }
tokio = { version = "1", features = ["full"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
serde = { version = "1", features = ["derive"] }
thiserror = "1"
http = "1"
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1"
[features]
default = []
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.leptos]
output-name = "my-app"
site-root = "target/site"
site-pkg-dir = "pkg"
style-file = "style/main.css"
assets-dir = "public"
site-addr = "127.0.0.1:3000"
reload-port = 3001
browserquery = "defaults"
env = "DEV"
bin-features = ["ssr"]
bin-default-features = false
lib-features = ["hydrate"]
lib-default-features = false
运行开发环境¶
快速上手:5 分钟最小示例¶
src/lib.rs:
use leptos::prelude::*;
#[component]
pub fn App() -> impl IntoView {
let (count, set_count) = signal(0);
let doubled = move || count.get() * 2;
view! {
<main>
<h1>"Leptos 计数器"</h1>
<p>"当前值: " {count}</p>
<p>"双倍值: " {doubled}</p>
<div class="buttons">
<button on:click=move |_| set_count.update(|n| *n -= 1)>
"-1"
</button>
<button on:click=move |_| set_count.set(0)>
"重置"
</button>
<button on:click=move |_| set_count.update(|n| *n += 1)>
"+1"
</button>
</div>
</main>
}
}
src/main.rs(SSR 版):
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use my_app::App;
let conf = get_configuration(None).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, {
let options = leptos_options.clone();
move || view! { <App /> }
})
.fallback(leptos_axum::file_and_error_handler::<App>)
.with_state(leptos_options);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
println!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service()).await.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {}
运行:
进阶用法¶
场景一:Server Functions¶
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Todo {
pub id: u32,
pub title: String,
pub completed: bool,
}
// Server Function: 在服务端执行,自动生成 API 端点
#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
// 这里的代码只在服务端运行
// 可以安全地访问数据库、文件系统等
let todos = sqlx::query_as!(Todo, "SELECT * FROM todos ORDER BY id")
.fetch_all(&pool())
.await?;
Ok(todos)
}
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<Todo, ServerFnError> {
let todo = sqlx::query_as!(
Todo,
"INSERT INTO todos (title, completed) VALUES ($1, false) RETURNING *",
title
)
.fetch_one(&pool())
.await?;
Ok(todo)
}
#[component]
fn TodoApp() -> impl IntoView {
// Resource: 自动调用 Server Function 并管理加载状态
let todos = Resource::new(|| (), |_| get_todos());
let add_action = Action::new(|title: &String| {
let title = title.clone();
async move { add_todo(title).await }
});
view! {
<h1>"Todo List"</h1>
<form on:submit=move |ev| {
ev.prevent_default();
let input = document().get_element_by_id("title").unwrap();
let title = input.dyn_ref::<web_sys::HtmlInputElement>().unwrap().value();
add_action.dispatch(title);
}>
<input id="title" type="text" placeholder="新任务..." />
<button type="submit">"添加"</button>
</form>
<Suspense fallback=move || view! { <p>"加载中..."</p> }>
{move || todos.get().map(|result| match result {
Ok(todos) => view! {
<ul>
{todos.into_iter().map(|todo| view! {
<li class:completed=todo.completed>
{todo.title}
</li>
}).collect_view()}
</ul>
}.into_any(),
Err(e) => view! { <p>"错误: " {e.to_string()}</p> }.into_any(),
})}
</Suspense>
}
}
场景二:路由¶
use leptos::prelude::*;
use leptos_router::components::*;
use leptos_router::path;
#[component]
fn App() -> impl IntoView {
view! {
<Router>
<nav>
<A href="/">"首页"</A>
<A href="/about">"关于"</A>
<A href="/users">"用户"</A>
</nav>
<main>
<Routes fallback=|| "404 页面未找到">
<Route path=path!("/") view=HomePage />
<Route path=path!("/about") view=AboutPage />
<Route path=path!("/users") view=UsersPage />
<Route path=path!("/users/:id") view=UserDetail />
</Routes>
</main>
</Router>
}
}
#[component]
fn UserDetail() -> impl IntoView {
let params = leptos_router::hooks::use_params_map();
let id = move || params.get().get("id").unwrap_or_default();
view! {
<h1>"用户详情: " {id}</h1>
}
}
场景三:表单处理与 Action¶
use leptos::prelude::*;
#[server(Login, "/api")]
async fn login(username: String, password: String) -> Result<String, ServerFnError> {
if username == "admin" && password == "password" {
Ok("登录成功".to_string())
} else {
Err(ServerFnError::ServerError("用户名或密码错误".to_string()))
}
}
#[component]
fn LoginForm() -> impl IntoView {
let login_action = ServerAction::<Login>::new();
let value = login_action.value();
view! {
<ActionForm action=login_action>
<label>
"用户名"
<input type="text" name="username" required />
</label>
<label>
"密码"
<input type="password" name="password" required />
</label>
<button type="submit">"登录"</button>
</ActionForm>
{move || value.get().map(|result| match result {
Ok(msg) => view! { <p class="success">{msg}</p> }.into_any(),
Err(e) => view! { <p class="error">{e.to_string()}</p> }.into_any(),
})}
}
}
场景四:错误处理¶
use leptos::prelude::*;
#[component]
fn ErrorHandling() -> impl IntoView {
let (count, set_count) = signal(0_i32);
// 可能产生错误的计算
let checked_div = move || -> Result<String, String> {
let c = count.get();
if c == 0 {
Err("不能除以零!".to_string())
} else {
Ok(format!("100 / {} = {}", c, 100 / c))
}
};
view! {
<ErrorBoundary fallback=|errors| view! {
<div class="error">
<h3>"出错了:"</h3>
<ul>
{move || errors.get().into_iter().map(|(_, e)| view! {
<li>{e.to_string()}</li>
}).collect_view()}
</ul>
</div>
}>
<p>{checked_div}</p>
</ErrorBoundary>
<button on:click=move |_| set_count.update(|n| *n += 1)>"+1"</button>
<button on:click=move |_| set_count.set(0)>"设为0(触发错误)"</button>
}
}
场景五:上下文(Context)与全局状态¶
use leptos::prelude::*;
#[derive(Clone)]
struct AppState {
theme: RwSignal<String>,
user: RwSignal<Option<String>>,
}
#[component]
fn App() -> impl IntoView {
// 在根组件提供上下文
let state = AppState {
theme: RwSignal::new("light".to_string()),
user: RwSignal::new(None),
};
provide_context(state);
view! {
<ThemeToggle />
<UserInfo />
}
}
#[component]
fn ThemeToggle() -> impl IntoView {
// 在任何子组件中获取上下文
let state = expect_context::<AppState>();
view! {
<button on:click=move |_| {
state.theme.update(|t| {
*t = if t == "light" { "dark".to_string() } else { "light".to_string() };
});
}>
"当前主题: " {move || state.theme.get()}
</button>
}
}
常见问题与排错¶
问题一:编译很慢¶
Rust 编译本身较慢,Wasm 编译更是如此。
优化建议:
# Cargo.toml - 开发时优化编译速度
[profile.dev]
opt-level = 0
[profile.dev.package."*"]
opt-level = 2 # 依赖包用 O2
# .cargo/config.toml - 使用更快的链接器
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
问题二:move 闭包和借用检查器¶
// 错误:count 被 move 后不能再使用
let (count, set_count) = signal(0);
let handler1 = move |_| set_count.set(count.get() + 1);
let handler2 = move |_| set_count.set(0); // 编译错误!
// 正确:Signal 实现了 Copy trait,可以多次 move
// 在 Leptos 0.7 中,Signal 类型是 Copy 的
let (count, set_count) = signal(0);
let handler1 = move |_| set_count.update(|n| *n += 1); // OK
let handler2 = move |_| set_count.set(0); // OK
问题三:Server Function 不工作¶
确保 ssr feature 正确配置,Server Function 需要在 cfg(feature = "ssr") 下编译才有实际实现。
问题四:Wasm 包体积过大¶
# Cargo.toml
[profile.release]
opt-level = "z" # 最小体积优化
lto = true # 链接时优化
codegen-units = 1 # 单编译单元
strip = true # 去掉调试符号
问题五:如何调试¶
// 在浏览器控制台输出
use leptos::logging::log;
log!("count = {}", count.get());
// 设置 panic hook 在控制台显示 Rust panic
console_error_panic_hook::set_once();
问题六:如何与 JavaScript 互操作¶
use wasm_bindgen::prelude::*;
// 调用 JavaScript 函数
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// 暴露 Rust 函数给 JavaScript
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
参考资源¶
- 官方文档:https://leptos.dev/
- Leptos Book:https://book.leptos.dev/
- GitHub:https://github.com/leptos-rs/leptos
- 官方示例:https://github.com/leptos-rs/leptos/tree/main/examples
- Leptos Discord:链接在 GitHub README 中
- cargo-leptos:https://github.com/leptos-rs/cargo-leptos
- Rust Wasm Book:https://rustwasm.github.io/docs/book/
- JS Framework Benchmark:https://krausest.github.io/js-framework-benchmark/