GLORY 指南
编写自定义组件。
实现 Widget 可为可复用的 Rust 值提供独立的构建逻辑、状态所有权、子组件和语义事件处理器。trait 本身很小;理解 build、Scope 和挂载生命周期,才能正确实现组件。
完整的 Counter
下面的组件与宿主无关,不包含 DOM 类型、浏览器监听器、HTML 字符串或原生窗口调用。通过 Web、SSR、Blitz 或内存宿主挂载 Counter,使用的是相同语义树和事件行为。
counter.rs
use glory::prelude::*;
pub struct Counter;
impl Widget for Counter {
fn build(&mut self, scope: &Scope) -> Result {
let count = scope.cage(0_i32);
let label = count.map(|count| format!("Count: {count}"));
col()
.class("counter")
.fill((
text().kind(TextKind::Heading(1)).content("Counter"),
text().content(label),
button()
.label("Increment")
.on(activate, move |event| {
if matches!(event, Event::Activate) {
count.revise(|count| *count += 1);
}
}),
))
.show_in(scope)
}
}必须实现的方法是 fn build(&mut self, scope: &Scope) -> Result。此处 Result 是 Glory 的 UI 结果别名,因此节点创建、子组件挂载等可能失败的操作可使用 ?。最后的 col().show_in(scope) 将根构建器挂载到 Counter 的作用域中,并返回该 Result。在 Widget 实现中直接返回 col() 会导致类型错误。
状态属于已挂载的 Scope
scope.cage(0_i32) 创建由本次挂载拥有的可变状态。返回的 Cage 是小型 Copy 句柄,可被派生标签和按钮处理器捕获。Counter 不需要 count 字段:状态由运行时拥有,生命周期与挂载 Scope 完全一致。每个单独挂载的 Counter 都从独立的零开始。
count.map 为 text().content 派生响应式 String。该构建器方法接受可转换为 Lotus 的值,因此既可使用静态 String,也可使用响应式数据源。事件处理器调用 revise 后,派生值和文本绑定被调度,build 不会重跑。避免在纯派生计算中创建 map 或其他响应式节点。
build 负责构建
不要期待 self 字段或普通 Cage 读取自动刷新界面。将配置存入 self,在 build 中创建有所有权的响应式状态,并为每个需要变化的属性安装绑定。
Activate 是语义事件
.on(activate, handler) 订阅 EventKind::Activate。回调接收类型化的 glory::render::Event,prelude 也会重新导出它。Event::Activate 表示控件激活请求,不会让共享组件依赖浏览器 MouseEvent 或键盘事件。宿主处理原生输入并将语义事件入队,再由 Glory 分派给注册的处理器。
Counter 中的 matches! 检查明确确认事件载荷,虽然仅注册 activate 的处理器通常只会收到 Event::Activate。其他控件使用其他事件及载荷:value_changed 携带文本和修订号,checked_changed 携带布尔值。共享组件逻辑不应通过直接调用 DOM 事件模拟输入;应驱动应用状态,或在测试中使用语义事件。
fill 接受 IntoChildren
View、Group、Button 和 Link 构建器通过 fill 接受子组件。IntoChildren 实现覆盖单个 Widget、异构组件元组、Vec<W>,以及表示无子组件的 ()。固定布局通常使用元组;需要单个异构向量时使用 Vec<Box<dyn Widget>>,响应式集合则使用 each 或 list。
可复用包装组件可在构造器中接受 impl IntoChildren,并保存生成的 glory::ui::Children。Children 是会被消耗的挂载输入,不能作为可克隆模板。由于 build 接收 &mut self 且只运行一次,应将其替换为空子列表,再把原值传给 fill,后者会将每个子组件挂载到新的语义父节点下。
panel.rs
use glory::prelude::*;
use glory::ui::{Children, IntoChildren};
pub struct Panel {
children: Children,
}
impl Panel {
pub fn new(children: impl IntoChildren) -> Self {
Self { children: children.into_children() }
}
}
impl Widget for Panel {
fn build(&mut self, scope: &Scope) -> Result {
// build takes &mut self, so consume the configured children once.
let children = std::mem::replace(&mut self.children, ().into_children());
view().class("panel").fill(children).show_in(scope)
}
}
pub fn page() -> impl Widget {
Panel::new((
text().kind(TextKind::Heading(1)).content("Account"),
text().content("Children can be any heterogeneous widget tuple."),
Counter,
))
}Panel::new((heading, body, Counter)) 可用,是因为三元素元组实现了 IntoChildren。多次调用 fill 会扩展构建器保存的子组件列表,不会替换之前的子组件。这与配置单个可替换属性槽的 class 和 style 不同。
class and style replace their previous values
对同一构建器调用两次 .class(...),会替换完整的空白分隔类名列表,不会追加。同样,.style(...) 会替换完整的类型化 Style,并移除该属性槽先前的绑定,无论新值是静态还是响应式都如此。外部样式表仍可通过 CSS 层叠组合样式,但两次构建器调用并不是合并 API。
需要组合多个字段时,应通过链式字段方法构造一个 Style。特别注意 row() 和 col():其初始 flex 显示和方向也只是一个 Style 值。替换样式时若不包含这些字段,就会移除便捷构造器的默认值。可以从 view() 开始,或在替换值中显式包含 display 和 flex_direction。
overrides.rs
// The second call replaces the complete class list, not just one token.
let button = button()
.class("primary")
.class("primary is-active"); // final classes: primary, is-active
// These fields are merged by the Style builder into one Style value.
let spacing = Style::new()
.padding(Edges::all(12.0))
.gap(8.0);
let panel = view().style(spacing);
// col() is a View with flex defaults. Replacing its Style must retain them.
let column = col().style(
Style::new()
.display(Display::Flex)
.flex_direction(FlexDirection::Column)
.gap(12.0),
);子组件与 Owner 相互关联,但并不等同
语义父子关系和响应式所有权有关,但并非同一概念。Panel 的 View 是其子组件的语义父节点。fill 中的每个子组件都挂载于新的子 Scope,构建它的作用域会保留该挂载。因此,子 Owner 跟随 Panel 的生命周期,同时仍能拥有自己的状态、订阅、任务和后代。
移除已挂载分支会释放其 Owner 和后代,使代数句柄失效,移除监听器与语义节点,并取消已入队的工作。在父回调中捕获子组件的 Cage 不会反转这个关系,也不会使子组件保持存活。状态需要比某个分支活得更久时,应从较长生命周期的 Scope 显式向下传递。
通常不需要手动调用 scope.child():fill、show_in、mount_with 和控制流控制器会建立并保留所需的子作用域。如果自定义控制器直接创建子 UI Scope,就必须在整个挂载期间保留该 Scope 或其 Mounted 守卫。释放守卫会释放子树。
cleanup.rs
widget(|scope| {
let value = scope.cage(String::new());
scope.on_cleanup(|| {
// Release application resources that belong to this mount.
})?;
text().content(value).show_in(scope)
})选择 trait、闭包或普通函数
组件具有可复用名称、配置、可接收的子组件,或需要基于 trait 组合时,实现 Widget,Counter 和 Panel 就是例子。本地组件需要自有状态,但不值得定义具名结构体时,可使用 widget(|scope| ...)。闭包必须实现 FnMut(&Scope) -> Result + 'static,每次挂载仍然只构建一次。
save_button.rs
use glory::prelude::*;
pub fn save_button() -> impl Widget {
widget(|scope| {
let saving = scope.cage(false);
let label = saving.map(|saving| {
if *saving { "Saving…" } else { "Save" }.to_owned()
});
button()
.label(label)
.on(activate, move |_| saving.revise(|saving| *saving = true))
.show_in(scope)
})
}无状态组合,或按参数配置并返回另一个组件时,普通 fn card(...) -> impl Widget 就足够了。函数需要 Scope 来分配本地 Cage 时,应返回 widget(...),不要在普通函数执行期间尝试读取状态。这些选择不改变更新模型:动态属性需要绑定,结构变化需要控制流。
回顾保留式语义树和更新模型 →挂载检查要点
在 build 的 Scope 中分配状态;用 show_in(scope) 完成根节点;保存的子组件只消耗一次;使用类型化 Event 处理器;并让 fill、show_in 或显式 Mounted 守卫维持每个子组件的生命周期。