Axum 与 Leptos 的集成是构建现代、高性能 Rust 全栈应用的首选组合。这种架构允许你使用一门语言(Rust)统一前后端,实现从服务器端渲染到客户端交互的无缝衔接。以下是基于最佳实践和行业经验的详细指南。
核心架构概览
在 Axum + Leptos 架构中,Leptos 负责视图层和业务逻辑,而 Axum 作为 HTTP 服务器,处理路由、中间件和与 Leptos 的集成。其核心优势在于支持 同构渲染:服务器端生成初始 HTML(SSR),客户端进行水合(Hydration)以接管交互。
一个典型的项目结构如下:
my-leptos-app/
├── Cargo.toml
├── index.html
└── src/
├── main.rs # Axum 服务器入口,集成 Leptos
├── app.rs # Leptos 根组件和路由定义
├── lib.rs # 共享类型和工具函数
├── api/
│ ├── mod.rs
│ └── handlers.rs # 纯后端 API 处理器(Axum)
└── components/ # Leptos 组件
第一步:项目配置与依赖集成
首先,在 Cargo.toml 中声明必要的依赖,确保版本兼容性 。
toml
[package]
name = "axum-leptos-starter"
version = "0.1.0"
edition = "2021"
[dependencies]
# 后端与服务器
axum = "0.7"
tokio = { version = "1.37", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["fs", "trace"] }
# Leptos 全栈核心
leptos = { version = "0.6", features = ["csr", "hydrate", "serde"] }
leptos_axum = "0.6" # 关键的集成桥接库
# 工具类
serde = { version = "1.0", features = ["derive"] }
cfg-if = "1.0"
[build-dependencies]
# 用于构建 WASM 目标
cargo-leptos = "0.6"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
第二步:Axum 服务器与 Leptos 路由集成
这是集成的核心。你需要在 Axum 应用中挂载 Leptos 的处理函数,以处理所有前端路由和静态资源 。
rust
// src/main.rs
use axum::{Router, routing::get};
use leptos::*;
use leptos_axum::{LeptosRoutes, generate_route_list};
use tower_http::services::ServeDir;
use std::net::SocketAddr;
// 导入你的 Leptos 应用根组件
use my_leptos_app::app::App;
#[tokio::main]
async fn main() {
// 1. 生成 Leptos 应用的路由列表
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);
// 2. 构建 Axum 应用路由器
let app = Router::new()
// 挂载 Leptos 应用的路由处理器,处理所有匹配的页面请求
.leptos_routes(&leptos_options, routes, App)
// 挂载纯后端 API 路由(不经过 Leptos)
.nest("/api", api::routes())
// 服务静态文件(如 WASM 包、CSS、图片)
.fallback_service(ServeDir::new(&leptos_options.site_root))
.with_state(leptos_options);
// 3. 启动服务器
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
println!("服务器运行在: http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
// 纯后端 API 模块
mod api {
use axum::{Router, routing::get, Json};
use serde_json::{json, Value};
pub fn routes() -> Router {
Router::new().route("/health", get(health_check))
}
async fn health_check() -> Json<Value> {
Json(json!({ "status": "ok" }))
}
}
第三步:构建 Leptos 同构应用
Leptos 应用需要被配置为同时支持 SSR(服务器端渲染)和 CSR(客户端水合)。
rust
// src/app.rs
use leptos::*;
// 根组件,定义应用的主要布局和路由
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>"Axum + Leptos 全栈应用"</title>
// Leptos 会自动注入 WASM 加载脚本和样式
</head>
<body>
<Router>
<main>
<Routes>
<Route path="/" view=|cx| view! { cx, <HomePage/> }/>
<Route path="/about" view=|cx| view! { cx, <AboutPage/> }/>
</Routes>
</main>
</Router>
</body>
</html>
}
}
// 主页组件示例
#[component]
fn HomePage(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<h1>"欢迎来到 Axum + Leptos 全栈应用"</h1>
<button on:click=move |_| set_count.update(|c| *c += 1)>
"点击次数: " {count}
</button>
}
}
// "关于"页面组件
#[component]
fn AboutPage(cx: Scope) -> impl IntoView {
view! { cx, <h2>"这是一个使用 Rust 构建的全栈应用"</h2> }
}
第四步:状态管理与服务器函数(Server Functions)
这是实现前后端无缝通信的关键。Leptos 的 服务器函数 允许你在组件中直接调用后端 Rust 函数,编译器会同时生成前端(通过 fetch 调用)和后端(Axum 路由)代码 。
rust
// src/lib.rs 或独立的模块中
use leptos::*;
use serde::{Deserialize, Serialize};
// 1. 定义共享的数据结构
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Todo {
pub id: u32,
pub title: String,
pub completed: bool,
}
// 2. 创建服务器函数
#[server(GetTodos, "/api")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// 这里是后端执行逻辑!可以安全地访问数据库、文件系统等。
// 模拟从数据库获取数据
Ok(vec![
Todo { id: 1, title: "学习 Leptos".into(), completed: true },
Todo { id: 2, title: "集成 Axum".into(), completed: false },
])
}
#[server(AddTodo, "/api")]
pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
// 这里执行插入数据库等操作
println!("添加待办事项: {}", title);
Ok(())
}
在组件中使用服务器函数:
rust
// src/components/todo_list.rs
use crate::{get_todos, add_todo, Todo};
use leptos::*;
#[component]
pub fn TodoList(cx: Scope) -> impl IntoView {
// 使用 create_resource 异步加载数据
let todos_resource = create_resource(
cx,
|| (),
|_| async move { get_todos(cx).await.unwrap_or_default() }
);
view! { cx,
<div>
<h2>"待办事项列表"</h2>
<Transition fallback=move || view! { cx, <p>"加载中..."</p> }>
{move || {
todos_resource.read(cx).map(|todos| {
view! { cx,
<ul>
{todos.iter().map(|todo| view! { cx,
<li>{&todo.title}
<input type="checkbox" checked={todo.completed} />
</li>
}).collect_view(cx)}
</ul>
}
})
}}
</Transition>
// 添加新待办事项的表单
<AddTodoForm />
</div>
}
}
第五步:性能优化与部署实践
-
构建优化:
- 使用
cargo leptos build --release进行生产构建 。 - 开启
lto和更高的优化级别以减小 WASM 体积。 - 利用
leptos_axum的静态文件服务,并对 WASM 和 CSS 文件设置长期缓存头。
- 使用
-
错误处理与日志:
- 在 Axum 层使用
tower_http::trace::TraceLayer进行请求日志记录。 - 为 Leptos 的
ServerFnError实现统一的错误转换,以返回结构化的 API 错误。
- 在 Axum 层使用
-
部署配置:
- 最终的产物是一个独立的、包含所有静态资源的 Rust 二进制文件。
- 可以使用 Docker 容器化部署,基础镜像推荐
rust:slim。 - 在
Cargo.toml中配置[package.metadata.leptos]段,指定输出目录和站点地址 。
对比与总结:为何选择 Axum + Leptos?
下表总结了该组合相较于其他方案的核心优势:
| 对比维度 | Axum + Leptos 组合 | 传统 Node.js + React 全栈 | Rust 纯后端 + JS 前端 |
|---|---|---|---|
| 语言统一性 | 是,前后端均为 Rust | 否,后端 JS/TS,前端 JS/TS | 否,后端 Rust,前端 JS/TS |
| 类型安全 | 端到端,共享数据结构与 API 定义 | 依赖额外工具(如 tRPC) | 断裂,需手动维护 API 契约 |
| 性能 | 极高,SSR 速度快,WASM 执行高效 | 良好 | 后端极快,前端依赖 JS 引擎 |
| 包体积 | 小,WASM 经优化后体积可控 | 较大 | 前端包体积大 |
| 开发体验 | 编译时检查,重构安全,但编译时间较长 | 热重载快,生态丰富 | 上下文切换,需维护两套工具链 |
| 学习曲线 | 需掌握 Rust 和 Leptos 响应式概念 | 较低,生态成熟 | 中等,需学习两套技术 |
最佳实践建议:
- 渐进采用:可以从将现有 Axum 项目的某个 API 端点替换为 Leptos 服务器函数开始。
- 关注编译时间 :在开发时,可以使用
cargo leptos watch来获得更好的热重载体验 。 - 善用共享模块 :将类型定义、工具函数和常量放在
src/lib.rs中,供前后端共用,这是统一类型安全的最大优势所在 。 - 社区与资源 :密切关注
leptos和leptos_axum的官方文档与示例,其生态正在快速发展 。