基于 Leptos 信号系统与浏览器事件构建多计数器响应式应用:counters 示例深度解读
【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos
counters 是 Leptos 仓库中的一个经典 CSR(客户端渲染)示例,用约 100 行 Rust 代码演示了响应式应用的核心骨架:信号(Signal)、派生计算、浏览器事件绑定、动态列表渲染与组件上下文通信。本文将结合该示例的源码、单元测试与端到端测试,完整讲解其实现原理,并给出可直接复现的运行与测试步骤。
示例概览:一个带增删改的多计数器应用
counters 示例 实现了一个基础但完整的响应式应用:页面上有三个操作按钮(Add Counter、Add 1000 Counters、Clear Counters),可以动态新增、批量新增或清空计数器;每个计数器由-1、+1两个按钮和一个可编辑输入框构成,支持单独增减或直接输入数值,并能通过x按钮从列表中移除;页面顶部实时汇总「总计数」与「计数器数量」。
它的教学价值在于集中展示了 Leptos 中几乎最常用的几类 API:
signal!宏创建响应式信号,ReadSignal/WriteSignal读写分离;ArcRwSignal在动态列表中传递共享可变状态;provide_context/use_context实现组件间通信;For组件高效渲染/复用列表项;on:click、on:input:target等浏览器事件绑定;prop:value属性绑定与RwSignal的双向绑定。
对应源码位于 examples/counters/src/lib.rs,入口挂载逻辑在 examples/counters/src/main.rs。
数据模型:以ArcRwSignal承载动态列表状态
const MANY_COUNTERS: usize = 1000; type CounterHolder = Vec<(usize, ArcRwSignal<i32>)>;CounterHolder是计数器的底层存储:一个Vec,每个元素是「自增 id + 计数器信号值」的元组。选择ArcRwSignal<i32>而非普通信号,原因在于Vec元素需要被For组件和子组件Counter同时持有——ArcRwSignal提供了可克隆、可共享的引用计数所有权语义,克隆一份引用不会复制内部状态,却能让多个组件读写同一个信号源。其完整实现见 reactive_graph/src/signal/arc_rw.rs(pub struct ArcRwSignal<T>)。
应用级的状态由两个信号管理:
let (next_counter_id, set_next_counter_id) = signal(0); let (counters, set_counters) = signal::<CounterHolder>(vec![]);next_counter_id负责为计数器分配自增 id(作为For的 key 与删除依据);counters持有整个列表,配合set_counters.update(...)原地修改。
在子组件Counter内部,通过RwSignal::from(value)把传入的ArcRwSignal包装成读写一体的RwSignal,从而在视图里直接使用{value}渲染、用value.update(...)修改。
组件上下文:provide_context打通父子通信
由于每个Counter都需要「删除自己」的能力,而删除操作作用于父级持有的counters列表,示例采用了 Leptos 的上下文机制:
#[derive(Copy, Clone)] struct CounterUpdater { set_counters: WriteSignal<CounterHolder>, } // 在 Counters 根组件中 provide_context(CounterUpdater { set_counters }); // 在 Counter 子组件中 let CounterUpdater { set_counters } = use_context().unwrap();provide_context把WriteSignal注入组件树,子组件通过use_context::<CounterUpdater>()取出同一份句柄,从而在点击x按钮时直接从父级列表中移除自己:
set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))这比逐层传递回调(callback)更简洁,也避免了组件数量增多时的 prop-drilling 问题。上下文类型的查找在 Leptos 中按组件树向上进行,Copy特性保证了传递的只是廉价句柄。
增删改三个核心操作的事件处理
示例用闭包直接绑定浏览器事件,每个闭包内部通过set_counters.update(...)原地修改信号,从而触发依赖它的视图自动更新:
let add_counter = move |_| { let id = next_counter_id.get(); let sig = ArcRwSignal::new(0); set_counters.update(move |counters| counters.push((id, sig))); set_next_counter_id.update(|id| *id += 1); };add_many_counters一次性追加 1000 个计数器,clear_counters直接清空列表:
let add_many_counters = move |_| { let next_id = next_counter_id.get(); let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| { let signal = ArcRwSignal::new(0); (id, signal) }); set_counters.update(move |counters| counters.extend(new_counters)); set_next_counter_id.update(|id| *id += MANY_COUNTERS); }; let clear_counters = move |_| { set_counters.update(|counters| counters.clear()); };事件绑定语法为on:click=add_counter、on:input:target=...。其中on:input:target是 Leptos 提供的「事件目标强类型」写法,让闭包参数ev直接携带target().value()方法,无需手动类型转换:
on:input:target=move |ev| { value.set(ev.target().value().parse::<i32>().unwrap_or_default()) }输入框使用prop:value=value把信号值作为 DOM 属性value绑定,因此既能在输入时写回信号,又能让+1/-1的修改实时反映到输入框与旁边的<span>{value}</span>。
响应式汇总与For动态列表
页面顶部的汇总文本是「派生计算」的典型写法——直接读取counters信号并返回字符串,闭包会被框架追踪依赖,任何计数变化都会触发重算与局部更新:
<span><For each=move || counters.get() key=|counter| counter.0 children=move |(id, value)| { view! { <Counter id value/> } } />key使用自增 id 而非列表下标,保证了「先删除后新增」等场景下条目的稳定复用;children接收(id, value)并转交给Counter组件渲染。
完整运行指南:从 Trunk 快速启动到 Cargo Make
快速启动
counters 是纯客户端渲染示例,最简启动命令只需 Trunk:
trunk serve --open执行后 Trunk 会编译 wasm 并在浏览器打开应用。其入口 HTML 为 examples/counters/index.html,通过data-trunk rel="rust"指令让 Trunk 完成 Rust 到 wasm 的构建与注入。
Cargo Make 方式
也可用cargo-make走与 CI 一致的流程。前置要求:
- 安装 Nightly Rust:
rustup toolchain install nightly - 添加 wasm 目标:
rustup target add wasm32-unknown-unknown(examples/counters/rust-toolchain.toml 已声明targets = ["wasm32-unknown-unknown"]) - 安装 Trunk:
cargo install trunk - 安装 Cargo Make:
cargo install --force cargo-make
然后进入示例目录执行:
cargo make ci # 初始化并测试示例 cargo make start # 启动开发服务器(默认 http://127.0.0.1:8080) cargo make stop # 停止由 start 启动的进程examples/counters/Makefile.toml 通过extend引入了 examples/cargo-make/main.toml、wasm-test.toml、trunk_server.toml与playwright-trunk-test.toml等公共任务定义,因此ci、start、stop等任务开箱即用。更完整的示例运行说明见 examples/README.md。
依赖与工具链说明
examples/counters/Cargo.toml 中,示例通过路径依赖指向仓库内的 Leptos 主 crate,并启用csr特性:
leptos = { path = "../../leptos", features = ["csr"] } console_error_panic_hook = "0.1.7" [dev-dependencies] wasm-bindgen-test = "0.3.42" wasm-bindgen = "0.2.93" web-sys = "0.3.70"其中console_error_panic_hook让 wasm 中的 panic 信息输出到浏览器控制台(在 examples/counters/src/main.rs 中调用console_error_panic_hook::set_once()启用),调试阶段非常有用;wasm-bindgen-test与web-sys用于浏览器内的单元测试。
测试矩阵:wasm 单元测试与 Playwright E2E
该示例同时提供两层测试,覆盖了从信号逻辑到真实浏览器交互的完整链路。
浏览器内单元测试(wasm-bindgen-test)
examples/counters/tests/web.rs 通过wasm_bindgen_test_configure!(run_in_browser)在真实浏览器环境中运行测试。核心用例inc完整模拟了用户操作并逐段断言 DOM:
- 挂载
Counters组件,断言初始 HTML 为「3 个按钮 + Total: 0 from 0 counters」; - 连续点击 3 次 Add Counter,断言出现 3 个
<li>计数器; - 分别点击第 1、2、3 个计数器的
+1按钮各 1、2、3 次,断言Total: 6且三个计数依次为 1、2、3; - 点击第一个计数器的
x按钮,断言列表剩 2 个且总数变为 5。
测试通过leptos::task::tick().await等待一次微任务刷新,再读取inner_html()比对,验证了「信号更新 → DOM 自动同步」的响应式闭环。
端到端测试(Playwright)
e2e 目录下按功能拆分了多个 spec 文件:view_counters、add_counter、add_1k_counters、increment_count、decrement_count、enter_count、clear_counters、remove_counter。以 add_counter.spec.ts 为例,它通过共享的CountersPage封装(fixtures/counters_page.ts)调用addCounter()三次,然后断言计数器数量文本为"3"。
这些测试配合cargo make的playwright-trunk-test任务运行,验证了按钮点击、输入框输入、批量新增与清空等完整用户路径在真实浏览器中的行为。
小结
counters 示例是一份「小而全」的 Leptos 入门范本:从signal!、ArcRwSignal、provide_context/use_context、For到on:click/on:input:target/prop:value,几乎覆盖了编写响应式客户端应用的全部核心 API;同时其双层测试结构(wasm 单元测试 + Playwright E2E)也为后续项目如何验证响应式逻辑提供了可复制的模式。想进一步研究底层信号实现,可继续阅读 reactive_graph/src/signal/arc_rw.rs;想对比同主题的完整交互,可参考仓库内其他示例(如 counter、counter_isomorphic、todomvc)。
【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考