ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Leptos 无宏编程实战:用纯 Rust 函数式 API 构建计数器组件(counter_without_macros 示例详解)

Leptos 无宏编程实战:用纯 Rust 函数式 API 构建计数器组件(counter_without_macros 示例详解) Leptos 无宏编程实战用纯 Rust 函数式 API 构建计数器组件counter_without_macros 示例详解【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos本文基于 Leptos 仓库中 examples/counter_without_macros/README.md 展开深入解析这个不借助任何宏的客户端渲染计数器示例。它以极简代码演示了 Leptos 无宏编程范式的完整套路——从html::{div, button, span}命令式构建 DOM、on(ev::click, ...)绑定类型安全事件到RwSignalEffect的响应式状态管理并且无需 nightly 编译器、可在 stable Rust 上直接构建。读完本文你将掌握如何用纯函数式 API 手写组件、如何在无view!宏的情况下组织响应式 UI、以及如何用 wasm-bindgen-test 对无宏组件做端到端浏览器测试。一、示例定位宏版本之外的另一条路Leptos 最为人熟知的是它的view!宏与#[component]宏——只需要写出类似 HTML 的语法编译器就会自动生成高效的 DOM 构建代码。仓库中的 examples/counter/src/lib.rs 就是这种经典写法#[component] pub fn SimpleCounter(initial_value: i32, step: i32) - impl IntoView { let (value, set_value) signal(initial_value); view! { div button on:clickmove |_| set_value.set(0)Clear/button spanValue: {value} !/span ... /div } }而 counter_without_macros 则提供了完全平行的另一套实现不写任何宏既不用view!也不用#[component]只调用 Leptos 暴露的普通 Rust 函数把组件写成一个普通到不能再普通的pub fn。正如其 README 所述This example is the same like thecounterbut its written without using macros and can be build with stable Rust.这句话点出了无宏路线的两个关键卖点功能等价它与counter示例实现的是完全相同的计数器交互Clear / -1 / 1不依赖宏展开因此对编译器的要求更低可以直接用 stable Rust 工具链构建示例的 Cargo.toml 中rust-version 1.75也印证了这一点无需像部分宏特性那样切换到 nightly。二、快速开始Trunk 一键运行示例 README 给出的启动方式非常简洁trunk serve --opentrunk是一个面向客户端渲染CSRWeb 应用的轻量构建工具与开发服务器。执行后它会读取 index.html 作为入口!DOCTYPE html html head link>use counter_without_macros::counter; /// Show the counter pub fn main() { console_error_panic_hook::set_once(); leptos::mount::mount_to_body(|| counter(0, 1)) }console_error_panic_hook::set_once()把 wasm 端的 panic 信息输出到浏览器控制台方便调试mount_to_body(|| counter(0, 1))把counter组件挂载到body。mount_to_body定义于 leptos/src/mount.rs其签名要求传入一个返回impl IntoView的闭包内部会创建响应式 owner 并调用mount_to(body(), f)。注意这个counter不是一个组件宏生成的东西它就是一个普普通通的pub fn counter(initial_value: i32, step: u32) - impl IntoView。源码注释对此做了非常直白的说明A component is really just a function call: it runs once to create the DOM and reactive system即组件本质上就是一次函数调用——函数运行一次创建出 DOM 与响应式系统。这正是不需要宏的根本原因Leptos 的#[component]宏只是把函数即组件的约定做了一层语法糖包装。3.1 用 HTML 构建器拼出 DOM无宏版本的核心在于leptos::html模块提供的类型化 HTML 构建器use leptos::{ ev, html::{button, div, span}, prelude::*, };在view!宏里div、button、span这样的标签会被宏展开为对应的构建器调用而在无宏写法中这些就是实实在在的函数div()、button()、span()。它们各自返回一个强类型的HtmlElement_可以通过链式方法继续组装div() .child(( button() .on(ev::click, move |_| count.update(Count::clear)) .child(Clear), button() .on(ev::click, move |_| count.update(Count::decrease)) .child(-1), span().child((Value: , move || count.get().value(), !)), button() .on(ev::click, move |_| count.update(Count::increase)) .child(1), ))这段代码信息量很大我们逐点展开1.child(...)的通用性child接收任何实现了IntoView的类型——字符串、HtmlElement_、数组、以及最多 26 个元素的元组。上面span的 children 就是一个三元组(Value: , move || count.get().value(), !)其中move || count.get().value()是一个响应式闭包函数作为视图每次计数变化时Leptos 会自动重新求值这段文本这正是文本即响应式数据的体现。2.on(ev::click, ...)的类型安全事件事件名称统一放在leptos::ev模块中源码注释给出了两条理由防止事件名字符串拼写错误有编译期检查允许回调获得正确的类型推断例如ev::click的回调会收到ev::MouseEvent。3.update(...)的原地修改点击回调通过count.update(Count::clear)等方式把方法引用直接传给RwSignal::update原地修改内部状态并触发依赖方更新。3.2 用 RwSignal 与 Effect 搭起响应式状态状态层同样不依赖宏pub fn counter(initial_value: i32, step: u32) - impl IntoView { let count RwSignal::new(Count::new(initial_value, step)); Effect::new(move |_| { leptos::logging::log!(count {:?}, count.get()); }); ... }RwSignalT是 Leptos 响应式原语中的可读写信号在 reactive_graph/src/signal/rw.rs 中定义为pub struct RwSignalT, S SyncStorage其内部由 arena 分配的ArcRwSignalT支撑。与signal()返回的读/写分离元组不同RwSignal把读与写合并在同一个句柄上非常适合状态对象整体更新的场景这里存的不再是裸的i32而是一个自定义的Count结构体见下文状态建模更贴近业务Effect::new(...)创建了一个副作用每当count变化时向控制台打印当前值浏览器中输出到 DevTools console。Effect是 Leptos 响应式系统中观察变化、执行副作用的标准入口这里相当于view!中{value}这类响应式文本之外、独立的响应式联动示例。3.3 业务模型普通 Rust 结构体 Count#[derive(Debug, Clone)] pub struct Count { value: i32, step: i32, } impl Count { pub fn new(value: i32, step: u32) - Self { Count { value, step: step as i32, } } pub fn value(self) - i32 { ... } pub fn increase(mut self) { self.value self.step; } pub fn decrease(mut self) { self.value -self.step; } pub fn clear(mut self) { self.value 0; } }Count是一个 100% 普通的 Rust 结构体不依赖任何 Leptos 类型、不带任何宏标注只负责纯粹的领域逻辑increase/decrease/clear三个操作与step步长语义。counter(0, 1)意味着初始值为 0、每次增减步长为 1测试中也能看到使用counter(0, 1)之外的初始值/步长组合来覆盖不同场景。这样的分层带来一个显著好处业务逻辑与视图框架解耦。Count可以被单独单元测试甚至可以在非 wasm 环境下复用响应式层只负责把 UI 事件翻译成对Count的方法调用再通过RwSignal的更新把结果推回视图。四、与宏版本 counter 的逐点对照维度counter宏版本counter_without_macros无宏版本组件定义#[component] pub fn SimpleCounter(...)pub fn counter(...) - impl IntoView模板view! { div button ... ... }div().child((button()..., span()..., ...))状态let (value, set_value) signal(initial_value);let count RwSignal::new(Count::new(...));事件绑定on:clickmove |_| ....on(ev::click, move |_| ...)响应式文本{value}move || count.get().value()编译器要求常规部分场景需 nightly仅需 stable RustCargo.toml 中rust-version 1.75可以看出两条路线在能力上是等价的相同的 DOM 结构、相同的交互语义、相同的响应式更新。差异仅在于语法糖——宏版本把构建器链与信号访问包装成类 HTML 的声明式语法而无宏版本把这一切显式化为普通函数调用。这也从侧面说明Leptos 的宏只是语法糖底层运行时 API 本身就是完整自洽的当你需要嵌入动态生成模板、编写库代码、或规避宏带来的编译依赖时无宏路线是一条可靠的退路。五、测试验证无宏组件同样可测无宏路线并没有牺牲可测试性。该示例在 tests/ 下提供两层测试正好覆盖纯逻辑与真实 DOM两个层次。5.1 纯逻辑单元测试business.rstests/business.rs 用rstest对Count的方法做了参数化测试对initial_value ∈ {-2,-1,0,1,2,3,4}、step ∈ {1,2,3}的多组组合逐一断言increase得到initial_value step、decrease得到initial_value - step、clear归零。因为Count与框架完全解耦这些测试可以像普通 Rust 测试一样cargo test直接跑无需浏览器环境。5.2 浏览器端到端测试web.rstests/web.rs 则通过wasm-bindgen-test在真实浏览器中驱动组件wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn should_increment_counter() { open_counter(); click_increment(); click_increment(); tick().await; // reactive changes run asynchronously, so yield briefly before observing the DOM assert_eq!(see_text(), Some(Value: 2!.to_string())); }要点包括tick().await响应式更新是异步调度到微任务中的因此在断言 DOM 文本前需要先让出执行权源码注释明确说明reactive changes run asynchronously, so yield briefly before observing the DOM借助document().evaluate(...)的XPath 查询//*[text()...]按文本内容定位元素并触发.click()从而模拟点击 Clear/-1/1 按钮的用户操作三个测试分别验证1两次后显示Value: 2!、-1两次后显示Value: -2!、先加后清零显示Value: 0!完整覆盖了组件的三条交互路径。该测试所需的依赖集中在 Cargo.toml 的[dev-dependencies]中wasm-bindgen、wasm-bindgen-test、pretty_assertions、rstest以及带HtmlElement、XPathResultfeature 的web-sys。配合 Makefile.toml 继承的wasm-test.toml任务即可纳入 CI 一键执行。六、小结无宏路线的适用场景从 counter_without_macros 这个示例可以总结出无宏编程的完整配方组件 普通函数返回impl IntoView入参即 propsDOM 构建器链html::{div, button, span}等类型化构建器 .child()/.on()组合状态 信号RwSignal或signal承载可变状态Effect::new处理副作用业务 纯结构体与框架无关的领域模型便于单元测试与复用。它适合以下场景项目要求纯 stable Rust 工具链希望规避宏展开带来的 nightly 依赖需要动态生成 UI 结构例如在循环或条件分支中拼接元素构建器链比宏模板更直接编写库或框架层代码时希望避免view!宏对下游用户编译环境的约束。与此同时宏版本在可读性与声明式表达上依然占优。两条路线在 Leptos 中并非对立关系——它们共享同一套响应式运行时信号、Effect、IntoView你完全可以按场景混用宏负责长得像模板的静态视图构建器负责需要编程式组装的动态部分。这正是 counter 与 counter_without_macros 两个示例并列存在的意义让开发者看到同一种能力的两副面孔再按需取用。【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进