DevTrendsDeveloper trends
Accent color

euv-dev /

euv

【开源自荐】一款声明式 Rust WebAssembly UI 框架 - Euv 版本基线:基于 euv v0.12.17(2026-07)当前仓库 euv-dev/euv 的实际实现。所有 API、宏、CLI 命令均与源码一致。 项目概述 Euv 是一个基于 Rust 语言开发的声明式、跨平台 UI 框架,构建在 WebAssembly 之上。框架采用"按需 Patch 虚拟 DOM + 精确依赖追踪的细粒度响应式"两条路径相结合的设…

DTLifestyle & entertainmentDTResearch & learningDTCLIDTLibrary / SDKDTTerminalDTWeb

About

【开源自荐】一款声明式 Rust WebAssembly UI 框架 - Euv 版本基线:基于 euv v0.12.17(2026-07)当前仓库 euv-dev/euv 的实际实现。所有 API、宏、CLI 命令均与源码一致。 项目概述 Euv 是一个基于 Rust 语言开发的声明式、跨平台 UI 框架,构建在 WebAssembly 之上。框架采用"按需 Patch 虚拟 DOM + 精确依赖追踪的细粒度响应式"两条路径相结合的设计:用 html! 宏编写类 JSX 的声明式视图,用 usesignal / computed! / watch! 构建自动追踪依赖的响应式系统,渲染层通过 data-euv-id 全局事件委托 + 局部 Patch 更新避免不必要的整树 Diff。 核心特性 声明式 UI:html! 宏编写类 JSX 语法,编译期类型检查;属性支持字符串、信号、if/match 三态表达式;Rust 关键字属性加 r# 前缀(r#type、r#loop);布尔属性必须传 Signal<bool>;data / aria 自动转 data- / aria- 细粒度响应式 Signal:usesignal Hook + 精确依赖追踪,Signal::get() 在 DynamicNode 渲染上下文内自动注册依赖,只有真正依赖该信号的 DynamicNode 会被标记为 dirty —— 不是全树广播 按需虚拟 DOM:VirtualNode 变体 Element | Text | Fragment | Dynamic | Empty;列表支持 key: 启用 Keyed Diffing;非列表场景走精确 Patch(attribute 级 HashMap diff),跳过完整 Diff Microtask 批处理:多次 Signal::set() 在同一微任务内自动合并为一次 dispatch;App::batch(|| { ... }) 提供手动抑制中间调度,闭包退出后才统一刷新 DOM 全局事件委托:基于事件冒泡 + data-euv-id 精确路由;非冒泡事件(blur / focus / mouseenter / mouseleave / scroll / resize / 媒体类 35 种)直绑 target;每种冒泡事件类型全局只注册一次监听器;handler 在 DOM 节点删除时自动清理,无内存泄漏 CSS-in-Rust:class! 宏原生支持伪类(hover / focus / nthchild 等)、伪元素(before / after)、媒体查询(media(...))、类继承(cbase();)、参数化类(cdynamiccolor(color: &str))、动态属性名({key}: {value};),snakecase 属性自动转 kebab-case;vars! / var! 提供类型安全的 CSS 变量定义与引用(注意是 vars! 不是 cssvars!) 计算属性 + 副作用:computed!(a, b, |a: T, b: T| -> R { ... }) 派生只读信号;watch!(a, b, |a, b| { ... }) 注册副作用,创建时立即执行一次 组件系统:#[component] 属性宏标注,组件函数签名 fn(node: VirtualNode<Props>) -> VirtualNode;支持 Props Down / Callback Up / 双向绑定(Shared Signal) CLI 工具链:内置 euv(来自 euv-cli crate),run / build / fmt 三种子命令,集成 wasm-pack,自动处理 --crate-path / --www-dir,支持热更新开发服务器 架构设计亮点 Workspace 架构 euv/ # 聚合入口:euv-core + euv-macros ├── core/ # 核心运行时:app / event / reactive / renderer / vdom ├── macros/ # 过程宏:html! / class! / vars! / var! / computed! / watch! / #[component] ├── cli/ # CLI 工具,binary 名 = euv ├── engine/ # 独立的 2D 游戏引擎(ECS / 固定步长 / 物理 / 碰撞 / 精灵 / 音频),可选 ├── ui/ # 跨 page 共享的 UI 组件库(euvbutton / euvcard / euvinput / ...) └── example/ # 示例 SPA:50 个 page(路由 / 布局 / 业务 / 全栈) | Crate | 职责 | 说明 | | --- | --- | --- | | euv | 聚合入口 | 对外暴露 App::usesignal / batch / mount / ...;重新导出 euv-core + euv-macros | | euv-core | 核心运行时 | VirtualNode、Signal 系统、Renderer、事件代理、HookContext、Scheduler | | euv-macros | 过程宏 | html! class! vars! var! computed! watch! #[component] | | euv-cli | CLI | run / build / fmt;集成 wasm-pack,热更新 Dev Server | | euv-engine | 2D 游戏引擎 | ECS / 固定步长 / 物理 / 碰撞 / 精灵 / 音频,可选依赖 | | euv-ui | 组件库 | 跨 page 共享的 euvbutton / euvcard / euvmodal / euvinput / euvbadge / ... | | euv-example | 示例 SPA | 50 个示例 page,覆盖各种使用场景 | 响应式系统架构 Signal::set(value) ── T: Clone + PartialEq ──┐ │ 值变化才更新 ▼ SignalInner.setvalue │ ▼ 遍历 listeners(已 swap-out 防重入) │ ▼ updatedependentsbysignaladdr ──► 标记 dirty │ ▼ App::scheduleupdate([...dirty ids]) │ ▼ queueMicrotask ─► dispatch │ ▼ 精确重新渲染每个 dirty DynamicNode │ ▼ attribute HashMap diff ─► DOM patch 关键设计要点: 1. 精确依赖追踪:Signal::get() 在 CURRENTTRACKINGDYNAMICID 激活时调用 adddependent(),建立 signal → DynamicNode 的反向依赖图。Signal::set() 时只标记实际依赖此 signal 的 DynamicNode,避免全树广播 2. Microtask 批处理:多次 set() 自动合并;App::batch(|| { ... }) 进一步抑制中间 dispatch,闭包退出时统一调度一次 3. Copy 语义:Signal<T> 实质是 usize(内部地址),实现 Copy;底层 Rc<SignalInner<T>> 由全局注册表持有,跨 async / 闭包 / setTimeout 自由拷贝不增加引用开销 4. usesignal Hook 语义:首次渲染时执行 init() 并存储;后续渲染返回同一 signal;hook index 由调用位置隐式决定(不可写在条件分支内,否则 index 错位) 5. 生命周期安全:Signal::deactivate() 仅清空 listeners/dependents,保留堆分配——因为 async 闭包可能持有旧 Signal 拷贝,释放内存会导致后续 .get() use-after-free;deactivate 后这些调用退化为 no-op 按需 Patch 策略 | 场景 | 算法 | 复杂度 | | --- | --- | --- | | key: 列表 | Keyed Diff | O(N) 建映射 + 最小 DOM 移动 | | 普通列表 | Positional Diff | O(min(M, N)) 逐位比较 | | 静态子树 | 不渲染(直接复用) | O(1) | | DynamicNode 重渲染 | Attribute HashMap diff | 只更新变化的属性 | | 初次挂载 | 全量构建 | — | Keyed Diff 细节: 构建 oldkey → oldindex HashMap 复用已有 DOM 节点,仅 Patch 属性差异 通过 insertbefore 最小化 DOM 移动操作 反向遍历移除废弃节点,避免索引偏移 全局事件委托机制 DOM Element └─ data-euv-id="42" ← 自动分配,跨 re-render 保持稳定 └─ Event Bubble ─► window 委托监听器 └─ dispatchdelegatedevent() 沿 target 向上找到最近的 data-euv-id └─ HandlerRegistry[(42, "click")] ─► HandlerSlot ─► 用户回调 冒泡事件:click / input / change / submit / keydown / keyup / mousedown / mouseup / mousemove / mouseover / mouseout / wheel / touchstart / touchmove / touchend / dragstart / drag / dragend / drop / pointer / focusin / focusout / copy / cut / paste / scroll(冒泡部分) 等,全部走 window 委托,每种事件类型全局只有一个监听器 非冒泡事件(35 种):blur / focus / mouseenter / mouseleave / load / error / scroll(element) / resize / beforeunload / 所有媒体事件(play / pause / seeked / timeupdate / ...)/ transitionend / animationend 等,直接绑在 target 元素上 精确路由:data-euv-id 在元素首次挂载时分配,跨 re-render 复用同一 ID,handler 通过 ID 在全局注册表查找 自动清理:DOM 节点被移除时,对应的 HandlerSlot 自动从注册表删除;Signal::deactivate() 同步清理 subscribe 闭包 开发便捷性 1. 声明式 UI 编写 rust use euv::; use wasmbindgen::prelude::; [wasmbindgen] pub fn main() { consoleerrorpanichook::setonce(); App::mount("#app", app); } fn app() -> VirtualNode { let count: Signal<i32> = App::usesignal(|| 0); let disabled: Signal<bool> = App::usesignal(|| false); html! { div { class: ccontainer() euvbutton { variant: EuvButtonVariant::Primary label: if { disabled.get() } { "Disabled" } else { "Increment" } onclick: move |: Event| { count.set(count.get() + 1); } } p { "Count: " { count.get() } } } } } 2. 响应式状态管理 rust fn pageconditional() -> VirtualNode { let showdetails: Signal<bool> = App::usesignal(|| false); let usertype: Signal<String> = App::usesignal(|| "guest".tostring()); // 计算属性:自动追踪 firstname / lastname 变化 // (此处仅示意,实际 firstname / lastname 需另行 usesignal) // let fullname: Signal<String> = computed!(firstname, lastname, |f: String, l: String| -> String { // format!("{} {}", f, l) // }); html! { div { class: cpagecontainer() euvbutton { variant: EuvButtonVariant::Primary label: "Toggle Details" onclick: move |: Event| { showdetails.set(!showdetails.get()); } } // 响应式 if——条件用 {} 包裹,依赖变化时自动重新求值 if { showdetails.get() } { div { class: ccard() "Hidden Details" } } // 响应式 match——必须含 通配分支 match { usertype.get().asstr() } { "guest" => { p { "Welcome, guest!" } } "user" => { p { "Hello, user!" } } => { p { "Welcome, admin!" } } } } } } 3. CSS-in-Rust 样式系统 rust // 类的定义统一在 ui/src/style/class/fn.rs(306 个跨 page 共享类) // 或项目内 example/src/style/class/fn.rs(仅本页使用) // page 的 view/fn.rs 只引用 cxxx() 函数,绝不写 class! 块 class! { pub ccard { background: var!(background); border: 1px solid var!(border); border-radius: 0; padding: var!(space-2xl); hover { background: var!(accent-muted); border-color: var!(accent); } // 唯一断点:移动端 < 768px media("(max-width: 767px)") { padding: var!(space-lg); } } // 参数化类 pub cdynamiccolor(color: &str) { color: {color}; background: var!(background); } // 继承 pub cprimarybutton { cbase(); background: var!(accent); color: var!(text-on-accent); hover { background: var!(foreground); // 覆盖 cbase 的 hover } } } // CSS 变量:vars! 定义,var! 引用 vars! { pub cthemelight { background: "#ffffff"; foreground: "#000000"; accent: "#000000"; accent-muted: "#ffffff"; border: "#000000"; text-on-accent: "#ffffff"; } pub cthemedark { background: "#000000"; foreground: "#ffffff"; accent: "#ffffff"; accent-muted: "#000000"; border: "#ffffff"; text-on-accent: "#000000"; } } // 引用:var!(name) 或 var!("name") 都行 class! { pub ccard { background: var!(background); border: 1px solid var!(border); } } 4. 计算属性与副作用 rust let count: Signal<i32> = App::usesignal(|| 0i32); let name: Signal<String> = App::usesignal(|| String::from("euv")); // 计算属性:firstname + lastname → fullname,自动追踪依赖 let firstname: Signal<String> = App::usesignal(|| String::from("Rust")); let lastname: Signal<String> = App::usesignal(|| String::from("WASM")); let fullname: Signal<String> = computed!(firstname, lastname, |f: String, l: String| -> String { format!("{} {}", f, l) }); // watch!:注册副作用,依赖变化时执行;创建时立即执行一次 watch!(count, name, |countval: i32, nameval: String| { websys::console::log1( &format!("count={}, name={}", countval, nameval).into() ); }); // 批量更新:闭包内的多次 set 只触发一次 DOM dispatch App::batch(|| { count.set(10); name.set("updated".tostring()); }); 5. CLI 开发工具链 sh 安装(包名 euv-cli,binary 名 euv) cargo install euv-cli 创建 / 启动开发服务器(热更新) euv run --crate-path . --www-dir ./www 构建发布 euv build --crate-path . --www-dir ./www 格式化宏代码 euv fmt --crate-path . 框架优势 1. 零成本抽象设计 编译期宏展开:html! class! vars! 等宏在编译时生成 Rust 代码,运行时零解析开销 类型安全:Props 自动提取(#[derive(Clone, Default)])、事件类型强校验、Signal<bool> 强制用于布尔属性,编译时消除 UI 逻辑错误 Copy 语义 Signal:Signal<T> 是 usize(内部地址),实现 Copy,仅复制指针地址;底层 Rc<SignalInner<T>> 由全局注册表持有,跨闭包 / async / setTimeout 无 GC 压力 依赖追踪在编译期不可能:Signal::get() 的运行时追踪是 euv 唯一"运行时开销",但配合 DynamicNode 边界可以做到零浪费——不在 DynamicNode 内调用 get() 就不注册依赖 2. 精细 DOM 更新策略 属性级 Patch:新旧属性 HashMap 对比,仅更新变化的属性 信号绑定属性:AttributeValue::Signal 自动订阅变更,DOM 节点移除时自动清理监听器(data-euv-signal-addrs 维护 signal 地址集合) DynamicNode 独立渲染:每个动态节点拥有独立的 Renderer 和 HookContext,实现局部更新 arm 切换检测:match 分支切换时自动执行 renderfullreplace,避免错误复用 3. 完善的生命周期管理 DOM 子树清理:cleanupdomsubtree 递归清理事件处理器、动态节点监听器、信号监听器 Hook 清理:match 分支切换 / 组件卸载时执行 cleanups 队列,防止信号泄漏 Signal 存活标记:alive 标志位 + deactivate() 路径确保已清理的 Signal 不再触发无效更新 4. 跨平台响应式布局 媒体查询:class! 宏原生支持 media("(max-width: 767px)") { ... } 语法,唯一断点 < 768px 视为移动端 CSS 变量主题:vars! 实现明暗主题,运行时通过 class 切换即可 技术栈对比 | 特性 | Euv | Yew | Dioxus | Leptos | | --- | --- | --- | --- | --- | | 声明式宏 | ✅ html! | ✅ html! | ✅ rsx! | ✅ view! | | 响应式系统 | ✅ Signal + 精确依赖追踪 | ❌(手动更新) | ✅ | ✅ | | 计算属性 | ✅ computed! | ❌ | ❌ | ✅ | | 副作用 | ✅ watch! | ❌ | ❌ | ✅ | | CSS-in-Rust | ✅ class! + vars! | ❌ | ❌ | ❌ | | 伪类/媒体查询 | ✅ 原生 | ❌ | ❌ | ❌ | | 全局事件委托 | ✅ 冒泡事件 | ❌ | ❌ | ❌ | | 组件系统 | ✅ #[component] | ✅ | ✅ | ✅ | | CLI 工具链 | ✅ run/build/fmt + wasm-pack | ❌ | ✅ | ✅ | | WASM 目标 | ✅ | ✅ | ✅ | ✅ | | Rust Edition | 2024 | 2021 | 2021 | 2021 | | 2D 游戏引擎 | ✅ euv-engine(ECS / 物理) | ❌ | ❌ | ❌ | 生态系统与社区 文档与学习资源 官方文档: API 文档:

Discovery history

  1. 阮一峰周刊·用户投稿

    May 27, 2026 · 1 observations

  2. 中国独立开发者·程序员版

    Jul 14, 2026 · 1 observations

Discuss this product