使用 Leptos + Axum 构建同构 Hacker News 应用:SSR 与 CSR 共存的完整实战示例解析
2026/9/13 2:01:39 网站建设 项目流程

使用 Leptos + Axum 构建同构 Hacker News 应用:SSR 与 CSR 共存的完整实战示例解析

【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos

本篇指南深入剖析 hackernews_axum 示例——一个基于 Leptos 框架、以 Axum 作为服务端的 Hacker News 克隆应用。它在一个仓库中同时展示了客户端渲染(CSR)与服务端渲染(SSR)+ 客户端水合(hydration)两种运行模式,覆盖了 Axum 集成、路由懒加载、服务端/客户端同构数据获取等核心工程能力。读完本文,你将掌握 Leptos 应用如何接入 Axum 服务器、如何通过 feature 区分构建目标,以及如何组织一套可复制的同构数据请求模式。

示例定位:一个仓库,两种渲染模式

根据 hackernews_axum 的 README,这个示例是 Hacker News 网站的基础克隆,核心价值在于:它展示了 Leptos 在同一个仓库中同时构建"客户端渲染应用"和"带水合的服务端渲染应用"的能力

它与仓库中另一个 hackernews 示例 的区别在于:本示例使用Axum作为 HTTP 服务器。这一点在 main.rs 中得到了印证——服务端入口直接引入了axum::routing::getRouter,并通过leptos_axum集成层完成 Leptos 应用与 Axum 路由的对接。

项目结构与关键文件

examples/hackernews_axum/ ├── Cargo.toml # feature 划分与 cargo-leptos 构建配置 ├── Makefile.toml # cargo-make 任务入口 ├── index.html # Trunk(CSR 模式)的 HTML 入口 ├── rust-toolchain.toml # 固定 wasm32-unknown-unknown 编译目标 ├── style.css # 示例样式(经 Lightning CSS 优化) ├── public/favicon.ico # 站点图标 └── src/ ├── main.rs # 服务端入口(SSR)与客户端入口(CSR)双实现 ├── lib.rs # 共享的 App 组件、shell 与路由声明 ├── api.rs # 同构数据获取层(SSR 用 reqwest,CSR 用 gloo-net) ├── error_template.rs # 错误模板 └── routes/ ├── nav.rs # 顶部导航栏组件 ├── stories.rs # 新闻列表页(top / new / show / ask / job) ├── story.rs # 新闻详情页(含嵌套评论树) └── users.rs # 用户信息页

从文件结构可以看出一个典型的分层思路:main.rs只负责"启动什么",lib.rs定义"应用是什么",api.rs抽象"数据从哪来",routes/目录承载各页面组件的实现。

快速启动:两条开箱即用的命令

原文档给出了两种快速启动方式,分别对应 Leptos 生态中两套主流构建工具:

命令适用模式工具
trunk serve --openCSR(默认 feature)Trunk
cargo leptos watchSSR + 水合cargo-leptos
  • trunk serve --open:以客户端渲染模式运行。默认 feature 为csr(见 Cargo.toml),Trunk 会编译 WASM 并启动开发服务器,适合纯前端快速预览。
  • cargo leptos watch:以服务端渲染模式运行。cargo-leptos 依据 Cargo.toml 中的[package.metadata.leptos]配置,分别以ssrfeature 编译 bin 目标、以hydratefeature 编译 lib 目标,并开启自动热重载。

环境准备(来自 Examples README)

在运行前需要准备以下工具链:

  1. 安装 Rust 与 Nightly 工具链:rustup toolchain install nightly
  2. 添加 WASM 编译目标:rustup target add wasm32-unknown-unknown(本示例的 rust-toolchain.toml 已固定该目标)
  3. 安装构建工具:cargo install trunk(CSR 模式)或cargo install cargo-leptos(SSR 模式)
  4. 可选安装cargo install cargo-make,然后可执行cargo make start/cargo make stop管理示例进程(见 Makefile.toml)

Feature 矩阵:csr / hydrate / ssr 三态切换

Hackernews_axum 的 Cargo.toml 是理解整个示例的关键:

[features] default = ["csr"] csr = ["leptos/csr"] hydrate = ["leptos/hydrate"] ssr = [ "dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:http", "leptos/ssr", "leptos_axum", "leptos_meta/ssr", "leptos_router/ssr", ]
  • csr:纯客户端渲染,不引入任何服务器依赖,所有网络请求通过gloo-net在浏览器内发起;
  • hydrate:为服务端输出的 HTML 提供水合入口,leptos::mount::hydrate_body会把事件绑定到已存在的 DOM 上(见 lib.rs);
  • ssr:开启 Axum、Tokio、tower 等服务器依赖,同时联动开启leptos_metaleptos_router的 SSR 能力。

注意csrssr相互独立、可分别编译,这正是"同一仓库两种模式"的工程基础。cargo-all-features的 denylist 配置(Cargo.toml)则避免了对 axum 等可选依赖的重复组合测试。

cargo-leptos 关键配置项

Cargo.toml 中[package.metadata.leptos]段的参数含义如下:

配置项示例值作用
output-namehackernews_axumJS/WASM 产物命名,默认取 crate 名
site-roottarget/site构建输出根目录,重建时会清空该目录全部内容
site-pkg-dirpkg站点根目录下存放 JS/WASM/CSS 产物的子目录
style-file./style.css源 CSS,经 Lightning CSS 优化后写入<site-root>/<site-pkg>/app.css
assets-dirpublic该目录下的文件会被复制到站点根目录
site-addr127.0.0.1:3000服务器监听地址与端口
reload-port3001自动重载监控端口
bin-features["ssr"]编译 bin 目标使用的 feature
bin-default-featuresfalse编译 bin 目标时不使用默认 feature
lib-features["hydrate"]编译 lib 目标使用的 feature
lib-default-featuresfalse编译 lib 目标时不使用默认 feature

这套配置实现了cargo leptos watch时"服务端用ssr、客户端用hydrate"的分目标构建,这也是 SSR + 水合应用的标准构建方式。

Axum 服务端集成:从路由生成到文件服务

main.rs 完整展示了 Leptos 应用挂载到 Axum 的过程:

#[cfg(feature = "ssr")] #[tokio::main] async fn main() { use axum::{routing::get, Router}; use hackernews_axum::{shell, App}; use leptos::config::get_configuration; use leptos_axum::{generate_route_list, LeptosRoutes}; let conf = get_configuration(Some("Cargo.toml")).unwrap(); let leptos_options = conf.leptos_options; let addr = leptos_options.site_addr; let routes = generate_route_list(App); let app = Router::new() .route("/favicon.ico", get(|| async { ... })) .leptos_routes(&leptos_options, routes, { move || shell(leptos_options.clone()) }) .fallback(leptos_axum::file_and_error_handler(shell)) .with_state(leptos_options); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app.into_make_service()).await.unwrap(); }

核心调用链可以拆解为四个环节:

  1. 读取配置get_configuration(Some("Cargo.toml"))从 Cargo.toml 的[package.metadata.leptos]段加载LeptosOptions,其中的site_addr127.0.0.1:3000)直接决定监听地址;
  2. 生成路由表generate_route_list(App)静态遍历App组件中声明的<Route>树,生成 Leptos 可识别的路径列表;
  3. 注册动态路由.leptos_routes(...)为这些路径统一注册服务端渲染入口——请求到达时由 Leptos 执行 SSR 输出完整 HTML,并内联水合脚本;
  4. 文件与错误兜底.fallback(leptos_axum::file_and_error_handler(shell))负责提供target/site下的静态资源(JS/WASM/CSS)以及错误页回退。

对应的 HTML 外壳由shell函数定义(lib.rs),其中<AutoReload>在开发期注入热重载脚本,<HydrationScripts>输出水合所需脚本,<MetaTags/>leptos_meta提供元信息出口:

pub fn shell(options: LeptosOptions) -> impl IntoView { view! { <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <AutoReload options=options.clone() /> <HydrationScripts options/> <MetaTags/> </head> <body> <App/> </body> </html> } }

此外还有一条Trunk(CSR)入口:当未启用ssrfeature 时,main.rs编译为纯客户端入口,直接mount_to_body(App),与 SSR 入口通过#[cfg(feature = "ssr")]互斥共存(main.rs)。

同构数据获取:一套代码,两种网络栈

Hacker News 类应用的核心是拉取外部 API。示例没有把数据获取逻辑写在组件里,而是收敛在 api.rs 中,并通过#[cfg]实现"同一函数签名、两种实现":

#[cfg(not(feature = "ssr"))] // 客户端:gloo-net + AbortController pub fn fetch_api<T>(path: &str) -> impl Future<Output = Option<T>> + Send + '_ { SendWrapper::new(async move { let abort_controller = SendWrapper::new(web_sys::AbortController::new().ok()); let abort_signal = abort_controller.as_ref().map(|a| a.signal()); on_cleanup(move || { if let Some(abort_controller) = abort_controller.take() { abort_controller.abort() // 离开页面时中止未完成的请求 } }); gloo_net::http::Request::get(path) .abort_signal(abort_signal.as_ref()) .send().await.ok()?.json().await.ok() }) } #[cfg(feature = "ssr")] // 服务端:reqwest pub async fn fetch_api<T>(path: &str) -> Option<T> { reqwest::get(path).await.ok()?.json().await.ok() }
  • SSR 端:在服务器进程内用reqwest直接请求上游 API,将数据渲染进 HTML 后返回;
  • CSR 端:在浏览器内用gloo-net请求,并利用web_sys::AbortControlleron_cleanup钩子实现"离开页面即中止在途请求"的体验优化;
  • 契约一致:两者都返回Option<T>,调用方(各页面组件)无需区分当前运行环境,这保证了同构代码的书写体验。

数据模型同样集中在 api.rs:StoryCommentUser三个结构体用serde反序列化上游 JSON。值得注意的细节是Story#[serde(alias = "type")] pub story_type: String(api.rs)——由于type是 Rust 关键字,这里通过 alias 映射外部字段,并利用#[serde(default)]兜底可选字段。

路由与页面组件:懒加载、资源与过渡动画

路由声明与懒加载

lib.rs 使用leptos_router声明了三条路由:

<Router set_is_routing> <div class="routing-progress"> <RoutingProgress is_routing max_time=Duration::from_millis(250)/> </div> <Nav /> <main> <FlatRoutes fallback=|| "Not found."> <Route path=(StaticSegment("users"), ParamSegment("id")) view={Lazy::<UserRoute>::new()}/> <Route path=(StaticSegment("stories"), ParamSegment("id")) view={Lazy::<StoryRoute>::new()}/> <Route path=OptionalParamSegment("stories") view=Stories/> </FlatRoutes> </main> </Router>
  • StaticSegment/ParamSegment/OptionalParamSegment组合出/users/:id/stories/:id/stories?page=N等路径;
  • Lazy::<UserRoute>::new()Lazy::<StoryRoute>::new()把用户页和故事详情页拆分为懒加载块,只有访问对应路由时才加载其 WASM/JS 代码;
  • RoutingProgress配合set_is_routing信号,在异步数据加载超过 250ms 时显示路由进度条,避免用户感知到卡顿。

列表页:Resource + Transition 的数据流

stories.rs 是列表页实现,展示了 Leptos 响应式数据流的标准写法:

let stories = Resource::new( move || (page(), story_type()), // 依赖:页码与分类变化时自动重取 move |(page, story_type)| async move { let path = format!("{}?page={}", category(&story_type), page); api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await }, );
  • Resource::new的第一个闭包声明响应式依赖,第二个闭包执行异步获取;
  • <Transition fallback=... set_pending>在切换页面时保留旧内容并显示加载态,数据到达后平滑更新,而不是整页闪烁;
  • <Show>用于在None(请求失败)时显示错误提示;
  • <For>story.id为 key 高效复用列表项 DOM;
  • 分页导航通过读取 URL 的 query 参数(use_query_map)与路径参数(use_params_map)实现,< prev/more >链接直接生成新的 URL,天然支持前进后退与刷新恢复。

详情页:lazy_route 与可折叠评论树

story.rs 展示了#[lazy_route]宏的用法——把"数据预取"与"视图渲染"拆成data()view()两个方法,实现进入路由前先并行加载数据的体验:

#[lazy_route] impl LazyRoute for StoryRoute { fn data() -> Self { let params = use_params_map(); let story = Resource::new_blocking( move || params.read().get("id").unwrap_or_default(), move |id| async move { api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await }, ); Self { story } } fn view(this: Self) -> AnyView { /* Suspense + 详情渲染 */ } }

评论部分采用递归组件<Comment>:每层评论通过signal(true)记录展开状态,点击[-]/[+] N replies切换折叠(story.rs),inner_html=comment.content直接渲染服务端返回的 HTML 内容。pluralize函数负责英文单复数的正确输出,细节上保证了与 Hacker News 原文案的一致性。

用户页与导航

users.rs 同样基于#[lazy_route],展示注册时间(created)、Karma 值、个人简介(inner_html渲染about)并外链到 Hacker News 的 submissions / threads 页面。nav.rs 提供Home / New / Show / Ask / Jobs五个导航链接,全部使用leptos_router::components::A组件实现客户端内跳转(不刷新页面)。

从示例到生产:可迁移的工程要点

综合源码可以提炼出四条可直接迁移到自研项目的模式:

  1. 双入口 + feature 矩阵:用csr / hydrate / ssr三个 feature 把客户端入口、水合入口、服务端入口隔离在同一 crate 中,#[cfg(feature = ...)]控制编译内容,[package.metadata.leptos]配置分目标构建(参考 main.rs 与 Cargo.toml);
  2. Axum 四步接入get_configuration读配置 →generate_route_list生成路由表 →.leptos_routes注册 SSR →.fallback(file_and_error_handler)兜底静态资源与错误页;
  3. 同构数据层:把网络请求收敛到一个模块,用 cfg 区分reqwest(服务端)与gloo-net + AbortController(客户端),调用方只依赖Option<T>返回契约(api.rs);
  4. 性能分层Lazy路由按需加载代码块、Resource::new_blocking提前预取数据、Transition保持界面连贯、RoutingProgress在慢网络下给出视觉反馈,四者组合即可获得接近原版 Hacker News 的浏览手感。

结语

hackernews_axum 示例在几十行配置与不到千行的业务代码中,完整覆盖了 Leptos 全栈开发的主干路径:Axum 服务端渲染、客户端水合、懒加载路由、响应式资源与同构数据获取。它以"Hacker News 克隆"这一足够真实的需求为载体,让每一种模式都能在 src 目录下找到对应的可运行实现。无论你是想为项目引入 SSR、还是评估 Leptos + Axum 的组合,这个示例都是最直接的起点。

【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询