GLORY 指南

架构与设计。

Glory 保留一棵类型化语义树,每个组件在挂载时构建一次,之后将响应式变化直接发送到受影响的节点。同一套应用模型可由浏览器、服务器、测试和原生宿主解释执行。

保留式语义树

挂载组件会创建具有稳定标识和类型化属性的语义节点。View、Text、Button、Input、Check、Image、Link 和 Group 描述界面的含义;共享 UI 层无需创建 DOM 元素或原生控件。当前渲染器保留这些节点,并将操作转换为宿主对应的实现。

每次挂载只执行一次 Widget::build。它创建初始结构,在传入的 Scope 中分配状态,并安装事件处理器和响应式绑定。后续状态变化不会再次调用 build,也不会生成第二棵树来进行比较。构建器使用 &mut self,因为挂载可能会消耗其中存储的子组件和声明。

counter.rs

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use glory::prelude::*; pub fn counter() -> impl Widget { widget(|scope| { let count = scope.cage(0_u64); col().fill(( text().kind(TextKind::Heading(1)).content("Counter"), text().content(count.map(|count| format!("Count: {count}"))), button() .label("Add one") .on(activate, move |_| count.revise(|count| *count += 1)), )) .show_in(scope) }) }

在这个示例中,count.map 创建一个被追踪的派生值,text().content 为它安装绑定。按钮处理器修改 Cage,运行时随后唤醒受影响的绑定,更新现有 Text 节点。构造新的 Counter 值表示一次新的挂载,不属于更新路径。

构建期间的普通读取不会建立响应式订阅

text().content(count.get().to_string()) 会将一次读取的值复制到静态属性中。需要文本随状态变化时,应使用 count.map(...)、其他 Bond 或 content_fn(...)。响应式读取只在 Bond、Binding 和 Effect 计算中追踪,不会追踪构建期间的任意读取。

从 Cage 修改到类型化 Patch

Cage 是具有所有权的可变数据源,不负责差异比较。revise 修改数据源并调度其直接依赖。观察到新值的文本绑定会为已知节点准备 glory::render::Patch::Text(Change::Set(new_text))。其他属性使用各自的变体,例如 Patch::Label、Patch::Classes、Patch::Style、Patch::Value 和 Patch::Checked。

这种区分同时影响正确性和性能。更新已经知道目标节点及字段,因此无需遍历 VDOM 来发现变化。类型化补丁还允许渲染器在执行操作前验证修订号、受控输入状态、支持的属性及宿主能力。写入会调度工作;运行时批处理以及宿主的事件或帧边界决定绑定和渲染提交何时执行。

这并不意味着所有计算都没有开销。派生计算仍会执行你编写的 Rust 代码,过宽的依赖仍可能带来多余工作。让派生值专注于所需数据,在合适时使用相等性检查,并通过带键的 each 或窗口化列表控制器处理集合,避免从一个 Cage 重建整个界面。

通过控制流改变结构

属性绑定不能增删语义节点。条件分支使用 when,普通带键集合使用 each,大型窗口化集合使用 list。这些控制器管理分支和行的挂载生命周期。结构工厂中的数据源读取与纯属性绑定分别处理。

structure.rs

1 2 3 4 5 6 7 8 9 10 11 12 13 14
let expanded = scope.cage(false); view().fill(( button() .label("Toggle details") .on(activate, move |_| expanded.revise(|open| *open = !*open)), when(expanded, || { group().fill(( text().kind(TextKind::Heading(2)).content("Details"), text().content("This branch has its own owned lifetime."), )) }), )) .show_in(scope)

Group 表示逻辑关系,View 具有布局盒

子组件需要逻辑父节点而不需要物理包装时,使用 group()。Group 没有布局盒、隐式无障碍角色,也不能作为物理几何或滚动目标。它仍可参与逻辑祖先关系和适用的逻辑事件路由,因此适合返回多个子组件,而不影响 flex 或 grid 布局。

需要布局盒、类型化样式、类名或结构性文档角色时,使用 view()。row() 和 col() 是带有默认 flex 方向的 View 构造器,并非特殊渲染原语。不能期待 Group 在布局计算中拥有尺寸;应查询具有布局盒的后代或使用 View。

groups.rs

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
use glory::prelude::*; pub fn actions() -> impl Widget { // Logical structure only: no wrapper box is created for the Group. group().fill(( text().content("Choose an action"), button().label("Save"), button().label("Cancel"), )) } pub fn toolbar() -> impl Widget { // A View has a box and accepts typed layout styles. row() .style( Style::new() .display(Display::Flex) .flex_direction(FlexDirection::Row) .gap(8.0), ) .fill((button().label("Save"), button().label("Cancel"))) }

Owner 是生命周期边界

每个 UI Scope 都包装一个核心 Owner,并增加语义构建上下文。通过该作用域创建的 Cage、Bond、绑定、副作用、监听器、任务和已挂载子组件都属于它的所有权树。show_in 和 fill 将挂载结果保留在父组件中。当控制流移除分支或应用释放挂载时,释放 Owner 会使该子树失效并取消其工作。

Cage 是带代数标识的 Copy 句柄,不会像 Rc 那样延长生命周期。在回调中捕获它不会让 Owner 保持存活;访问已退役句柄会失败,不会指向复用后的值。Owner::on_cleanup 用于注册显式清理,子级先于父级清理。长生命周期回调需要可选的 Scope 引用时,优先使用 scope.downgrade(),避免强引用 UI 作用域。

所有权也解释了组件状态为何相互独立。挂载两次 Counter 会在两个子作用域中执行两次 build,分配两个不同的 count Cage。只有显式捕获较长生命周期 Owner 中的 Cage 时,它们才共享应用状态。普通 fill 和 show_in 会管理子作用域;手动创建子 UI 作用域时,必须在其 UI 应保持挂载的整个期间保留它。

了解 Cage、Bond 和 Lotus 的所有权 →

一套语义模型,可替换的宿主

共享组件描述语义和行为,宿主提供呈现及执行服务。web 特性通过浏览器 DOM 和输入解释语义操作。ssr 特性同步序列化 HTML 或版本化 Snapshot 片段;事件处理器不会变成可执行 JavaScript。可选 Blitz 宿主通过原生 CSS 布局、文本、绘制和桌面窗口集成解释相同语义操作。

App::memory 为语义测试提供参考解释器。它可以测试挂载、补丁和类型化事件,但不产生浏览器或原生像素。在另一端,WebApp 和 Blitz 桌面 Shell 分别维持宿主生命周期,驱动输入、刷新、帧和清理。

hosts.rs

1 2 3 4 5 6 7 8 9 10 11
// Native SSR produces a versioned fragment for later browser adoption. let snapshot = glory::ssr::snapshot(app())?; serve_html(snapshot.html); // The browser mounts the same widget definition into a DOM root. let web = glory::web::launch(root, app())?; keep_web_app_alive(web); // A memory host is useful for semantic tests; it does not paint pixels. let app = App::memory(); app.mount(app_widget())?;

可移植性是语义和类型化样式的约定,不保证所有引擎输出相同像素。请检查各宿主支持的样式配置和交互能力。将 DOM 查找、HTTP 服务、窗口创建等平台职责放在宿主入口中,使共享组件保持可复用。

理解运行模型

构建一次结构,显式绑定可变属性,让控制流管理结构变化,并由选定宿主解释类型化语义操作。从 Cage 修改到 Patch 的过程中,没有隐藏的渲染函数重跑或 VDOM 协调步骤。