Leptos 同构计数应用实战:从浏览器调用服务端函数(counter_isomorphic 示例全解)
2026/9/13 13:32:57 网站建设 项目流程

Leptos 同构计数应用实战:从浏览器调用服务端函数(counter_isomorphic 示例全解)

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

本篇技术指南围绕 Leptos 仓库中的 counter_isomorphic 示例 展开,系统讲解如何在 Leptos 中实现"同构(Isomorphic)"开发:让同一套#[server]函数同时以服务端实现与浏览器端调用两种身份存在,浏览器触发、服务端执行、结果回传,并借此构建单用户、表单驱动与多用户实时三类计数器。读完本文,你将掌握cargo leptos watch的启动方式、ssr/hydrate双 feature 构建、Action+Resource的失效刷新模式,以及基于 Server-Sent Events(SSE)的多用户实时更新方案。

示例简介:什么是"同构函数"

官方 README 对该示例的定位一句话即可概括:

This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.

即"在浏览器中调用一个服务端函数并拿到返回值"。这里的"同构"不是指在客户端与服务端各写一份实现,而是同一个函数体经过编译期 feature 分支,在服务端渲染(SSR)时以真实逻辑执行、在浏览器端(hydrate)时以fetch请求的形式被调用,两边共用同一套函数签名与类型定义。

在 counters.rs 中可以看到这种"一份定义、双端可用"的典型写法:

#[server] #[cfg_attr(feature = "ssr", instrument)] pub async fn get_server_count() -> Result<i32, ServerFnError> { use ssr_imports::*; Ok(COUNT.load(Ordering::Relaxed)) }

#[server]宏(由 leptos_macro 提供)会自动生成两套代码:在ssrfeature 下编译为真实的异步实现;在客户端编译时则生成一个封装了 HTTP 调用(URL 即该函数的 API 端点)的桩函数,调用者完全无感知。返回值统一使用Result<T, ServerFnError>包装错误,这也是 README 所说的"从浏览器运行服务端函数并接收结果"的底层机制。

快速开始与运行环境

示例 README 给出的快速启动命令只有一个:

cargo leptos watch

它需要 cargo-leptos,其中明确了完整的前置条件与操作步骤:

  1. 安装 Rust 与 Nightly 工具链,并为当前工具链添加 WASM 目标:
    rustup toolchain install nightly rustup target add wasm32-unknown-unknown
  2. 安装 Cargo Make(可选但推荐,用于 CI 与一键启停):
    cargo install --force cargo-make
  3. 进入示例目录后,使用cargo make ci完成环境准备与测试,用cargo make start启动,用cargo make stop结束进程。

Makefile.toml 展示了该示例如何接入 cargo-make 体系:通过extend字段继承 examples/cargo-make/main.toml 与 examples/cargo-make/cargo-leptos.toml,其中start-client任务本质上执行的仍是cargo leptos watch。默认情况下,客户端页面会运行在http://127.0.0.1:3000(SSR 场景)或http://127.0.0.1:8080(CSR 场景),具体以控制台输出为准。

项目结构与双 feature 构建配置

counter_isomorphic是典型的 Leptos SSR + hydrate 项目,目录结构如下:

examples/counter_isomorphic/ ├── Cargo.toml # features 与 cargo-leptos 构建元数据 ├── Makefile.toml # cargo-make 任务入口 ├── public/ # 静态资源(favicon.ico) └── src/ ├── main.rs # Actix Web 服务端入口(SSR 侧) ├── lib.rs # hydrate 入口(客户端侧) └── counters.rs # 核心业务:server functions + 三个计数器组件

lib.rs 声明了crate-type = ["cdylib", "rlib"](见 Cargo.toml),这是 Leptos 同构应用的标准配置:rlib供服务端链接,cdylibwasm-bindgen生成 WASM 产物。客户端入口通过#[wasm_bindgen]暴露hydrate()

#[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() { use crate::counters::Counters; _ = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); leptos::mount::hydrate_body(Counters); }

服务端入口 main.rs 使用 Actix Web 构建 HTTP 服务,核心链路包括:get_configuration(None)读取构建配置 →generate_route_list(Counters)生成路由表 →leptos_routes(...)注册 Leptos 路由 →Files::new("/", site_root)托管静态资源,并在<head>中注入AutoReload(开发热重载)与HydrationScripts(水合脚本)。

对应的 Cargo.toml 定义了三个关键 feature:

  • hydrate:开启leptos/hydrate,用于编译浏览器端 WASM;
  • ssr:开启leptos/ssrleptos_actixactix-webactix-filestracing,用于编译服务端;
  • bin-features = ["ssr"]lib-features = ["hydrate"]:告知 cargo-leptos 分别用哪组 feature 编译可执行文件与库,从而实现"一份代码、双端构建"。

[package.metadata.leptos]段还给出了站点根目录site-root = "target/site"、产物目录site-pkg-dir = "pkg"、静态资源目录assets-dir = "public"、监听地址site-addr = "127.0.0.1:3000"与热重载端口reload-port = 3001等关键参数。需要特别注意的是注释中的警告:当不使用 cargo-leptos 运行时,site-root必须改为".",否则计数器功能无法正常工作。

服务端共享状态:跨连接的数据来源

三个计数器的数据都存储在同一份服务端变量中,这是理解整个示例的起点。counters.rs 中通过ssr_imports模块维护了进程级共享状态:

#[cfg(feature = "ssr")] pub mod ssr_imports { pub use broadcaster::BroadcastChannel; pub use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::LazyLock; pub static COUNT: AtomicI32 = AtomicI32::new(0); pub static COUNT_CHANNEL: LazyLock<BroadcastChannel<i32>> = LazyLock::new(BroadcastChannel::<i32>::new); }
  • COUNTAtomicI32)保存计数器的绝对值,对所有连接共享——这正是 README 中"The value is shared across connections"的出处,也是"打开另一个浏览器标签页即可验证"的原因;
  • COUNT_CHANNELBroadcastChannel<i32>,来自broadcastercrate)负责把每一次变更广播给所有 SSE 订阅者,供多用户计数器使用。

三个 server function 全部读写这份共享状态:

  • get_server_count():读取当前值;
  • adjust_server_count(delta: i32, msg: String):累加/累减并广播新值,同时println!("message = {:?}", msg)打印客户端传入的消息;
  • clear_server_count():清零并广播。

值得留意的是adjust_server_count的入参设计:delta: i32msg: String这种"具名参数"正好为后面的表单提交模式提供了数据载体。

三种计数器模式:由浅入深的同构调用范式

页面通过 Leptos Router 在三个路由间切换(见 counters.rs 的Counters组件):""(Simple)、"form"(Form-Based)与"multi"(Multi-User)。

模式一:Simple Counter —— Action + Resource 的失效刷新

这是典型的单用户 CRUD 模式(源码注释明确写到 "This is the typical pattern for a CRUD app")。其核心思路是:Action提交写操作,用Resource依赖写操作的版本号自动重取读接口

let dec = Action::new(|_: &()| adjust_server_count(-1, "decing".into())); let inc = Action::new(|_: &()| adjust_server_count(1, "incing".into())); let clear = Action::new(|_: &()| clear_server_count()); let counter = Resource::new( move || { ( dec.version().get(), inc.version().get(), clear.version().get(), ) }, |_| get_server_count(), );

关键机制在于version()Action每次成功 dispatch 都会递增内部版本号(见 reactive_graph/src/actions/action.rs 的version()实现)。当三个version()组成的元组任一发生变化时,Resource的 source 失效并重新执行get_server_count(),把服务端最新值拉回客户端。视图层再配合<Suspense>包裹读取结果:

<span>"Value: " <Suspense>{counter} "!"</Suspense></span>

ErrorBoundary负责捕获服务端函数返回的错误并格式化展示,保证调用失败时 UI 不崩溃。按钮通过on:click触发clear.dispatch(())dec.dispatch(())inc.dispatch(())完成操作。

模式二:Form Counter —— ServerAction + ActionForm 的表单驱动

该模式"与 Simple Counter 行为一致,但使用 HTML 表单提交动作"(README 与页面文案原话为 "When progressively enhanced, it should behave identically to the 'Simple Counter'")。它的核心是#[server]宏自动生成的结构体——即函数名的 PascalCase 版本:

let adjust = ServerAction::<AdjustServerCount>::new(); let clear = ServerAction::<ClearServerCount>::new();

ServerAction定义于 leptos_server/src/action.rs,它封装了服务端函数并可直接与<ActionForm>绑定。注释中有一句点睛之笔:"calling a server function is the same as POSTing to its API URL",因此完全可以用原生表单代替 JS 点击事件:

<ActionForm action=adjust> <input type="hidden" name="delta" value="-1"/> <input type="hidden" name="msg" value="form value down"/> <input type="submit" value="-1"/> </ActionForm>

这里inputname#[server]函数参数一一对应(deltamsg),表单提交即等价于带具名参数的 POST 请求。失效刷新逻辑与模式一相同:Resource依赖adjust.version()clear.version(),任何一次表单提交都会触发重新获取计数。由于整个交互走标准 HTML 表单,该模式天然具备"渐进增强"(progressive enhancement)特性。

模式三:Multi-User Counter —— SSE 实时广播

第三种模式解决的是多用户并发问题:当一个用户修改计数时,其他所有在线页面都要实时刷新。实现方式在 counters.rs 中明确标注:"It relies on a stream of server-sent events (SSE) for the counter's value",并注明这是"live chat、collaborative editing 等实时应用的原语模式"。

服务端由 main.rs 的/api/events路由提供 SSE 流:

#[get("/api/events")] async fn counter_events() -> impl Responder { let stream = futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) }) .chain(COUNT_CHANNEL.clone()) .map(|value| { Ok(web::Bytes::from(format!( "event: message\ndata: {value}\n\n" ))) as Result<web::Bytes> }); HttpResponse::Ok() .insert_header(("Content-Type", "text/event-stream")) .streaming(stream) }

流的第一帧是当前计数(新订阅者立即获得初始值),之后每次COUNT_CHANNEL广播新值都会推送给所有订阅者。客户端(hydrate分支)通过gloo_net::eventsource::futures::EventSource订阅该流:

let mut source = SendWrapper::new( gloo_net::eventsource::futures::EventSource::new("/api/events") .expect("couldn't connect to SSE stream"), ); let s = ReadSignal::from_stream_unsync( source.subscribe("message").unwrap().map(|value| /* 解析 data */), ); on_cleanup(move || source.take().close());

这里用SendWrapper包裹浏览器端非Send的流对象以满足编译约束,用ReadSignal::from_stream_unsync把事件流转换为响应式信号,on_cleanup在组件卸载时关闭连接。SSR 分支则退化为一个静态信号占位,仅用于保证首屏渲染不报错——这也再次体现了同构代码通过#[cfg]在不同构建目标下各取所需的编排能力。

从源码看同构调用的完整链路

综合三个模式,可以把"浏览器 → 服务端函数 → 结果回传"的完整链路梳理为:

  1. 编译期#[server]宏在客户端生成带 HTTP 请求逻辑的桩函数,在服务端生成真实实现;ssrhydrate两个 feature 互斥地参与各自目标的编译(Cargo.toml 的 feature 声明保证了这一点);
  2. 运行时:客户端调用get_server_count()/adjust_server_count(...)时,实际上向对应 URL 发起请求(与表单 POST 等价);
  3. 状态同步:写操作经Action/ServerAction提交,读操作由Resource依赖version()自动触发;实时场景则叠加 SSE 广播通道,把服务端共享状态的变化推送给所有连接;
  4. 渲染Suspense处理异步值,ErrorBoundary兜底错误,二者共同保证水合(hydration)阶段的体验一致性。

Action/Resource细节感兴趣的读者,可以在 reactive_graph/src/actions/action.rs 中查看version()Action<I, O>的定义;ServerAction的具体封装则在 leptos_server/src/action.rs。

小结

counter_isomorphic虽然体量不大,却浓缩了 Leptos 服务端集成中最核心的三套范式:Action + Resource 的失效刷新(单用户 CRUD 的通用骨架)、ServerAction + ActionForm 的渐进增强表单(无 JS 亦可工作的提交路径),以及SSE 实时广播(多人在线协作的基础)。三套范式共享同一份#[server]函数与同一份服务端状态,恰好完整诠释了"同构"二字:写一次函数、双端复用、状态收敛于服务端。

如需动手验证,按 README 指引执行cargo leptos watch启动后,先在第一个页面调整计数,再打开第二个浏览器标签页观察值的变化;随后切到 Multi-User 页面,在任意标签页点击按钮,即可看到 SSE 实时同步的效果。

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

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

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

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

立即咨询