GLORY 指南

Web 应用。

Glory 浏览器应用是由 wasm-bindgen 启动的 wasm32 Rust 库。宿主入口找到空的 web_sys::Element,挂载共享组件树,保留 WebApp 守卫,并通过 HTTP 提供生成的 JavaScript 和 WASM。

选择 wasm32 Web 特性

DOM 应用需启用 Glory 的 web 特性,它选择 HTML 映射器和浏览器宿主;共享 Counter 本身仍只需要 glory::prelude::*。添加 wasm-bindgen 以导出启动函数,并为挂载查找添加带 Window、Document 和 Element 的 web-sys。编译后使用的 wasm-bindgen CLI 必须与锁定的 Rust crate 版本一致。

Cargo.toml

1 2 3 4 5 6 7 8 9 10 11 12 13 14
[package] name = "my_glory_app" version = "0.1.0" edition = "2024" [lib] crate-type = ["cdylib", "rlib"] [dependencies] glory = { version = "0.3.1", default-features = false, features = ["web"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "=0.2.129" web-sys = { version = "=0.3.106", features = ["Window", "Document", "Element"] }

cdylib 输出是浏览器加载的 WASM 库;rlib 使代码也可用于原生测试和服务端渲染。使用 --target wasm32-unknown-unknown 构建浏览器入口。旧的 web-csr 和 web-ssr 特性名不属于当前 API,应选择 web、ssr,以及按需选择 hydrate。

找到根元素并调用 launch

launch(root, widget) 将已构造的 Widget 挂载到 web_sys::Element。下面的完整入口使用自定义组件指南中的 Counter。将 Glory 错误转换为 JsValue,使模块启动失败时能报告有效信息,避免静默留下空白页面。

src/web.rs

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
use glory::prelude::*; use std::cell::RefCell; use wasm_bindgen::prelude::*; thread_local! { static APP: RefCell<Option<glory::web::WebApp>> = const { RefCell::new(None) }; } #[wasm_bindgen(start)] pub fn start() -> Result<(), JsValue> { let root = web_sys::window() .and_then(|window| window.document()) .and_then(|document| document.get_element_by_id("app")) .ok_or_else(|| JsValue::from_str("missing #app mount element"))?; let app = glory::web::launch(root, crate::Counter) .map_err(|error| JsValue::from_str(&error.to_string()))?; APP.with(|slot| *slot.borrow_mut() = Some(app)); Ok(()) }

WebApp 是生命周期守卫

不要让返回的 WebApp 在 start 结束时离开作用域。释放它会释放 DOM、观察器、原生监听器、动画帧和所属异步任务。应将其保存在应用状态或 thread_local 守卫中,持续到界面不再需要运行。

使用 launch_with 构建根作用域状态

launch_with(root, factory) 接受 FnOnce(&Scope) -> W。构造根组件时也需要在根作用域分配 Cage 或其他状态,可使用它。工厂返回组件,不自行调用 show_in。组件值已经存在时,launch(root, widget) 是便捷的等价形式。

launch_with.rs

1 2 3 4 5 6 7 8 9 10
let app = glory::web::launch_with(root, |scope| { let count = scope.cage(0_u64); col().fill(( text().content(count.map(|count| format!("Count: {count}"))), button() .label("Add one") .on(activate, move |_| count.revise(|count| *count += 1)), )) })?; APP.with(|slot| *slot.borrow_mut() = Some(app));

两者均返回 Result<WebApp>。工厂形式不是重复执行的渲染回调:它与 Widget::build 一样只在挂载时运行一次。此时安装的响应式值会在启动后通过绑定更新保留式语义树。

CSR 挂载元素必须为空

launch 和 launch_with 需要空且未被占用的根元素。若元素有任何子节点或已存在 data-gly-app 属性,就会拒绝。文本节点也算子节点,因此 CSR 挂载元素内不能包含空白、注释、回退内容或服务端标记。该检查可防止客户端渲染静默删除本应水合的内容。

index.html

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Glory app</title> </head> <body> <main id="app"></main> <script type="module"> import init from "./my_glory_app.js"; await init(); </script> </body> </html>

模块导入 wasm-bindgen 生成的 JavaScript 并等待初始化。随后 #[wasm_bindgen(start)] 函数运行,找到 #app 并挂载 Glory。Rust 启动逻辑只需查找根元素,无需自行创建 DOM;语义 Web 渲染器会创建和更新应用 DOM。

构建绑定并通过 HTTP 提供文件

将库编译为 WASM,以 --target web 运行 wasm-bindgen,并将 index.html 放在生成的 .js 和 _bg.wasm 文件旁。随后为输出目录启动 HTTP 服务器,在浏览器中打开显示的 localhost 地址。

terminal

1 2 3 4 5 6 7 8 9 10 11
rustup target add wasm32-unknown-unknown cargo build --lib --target wasm32-unknown-unknown # Use a wasm-bindgen-cli version matching the locked wasm-bindgen crate. wasm-bindgen \ --target web \ --out-dir dist \ target/wasm32-unknown-unknown/debug/my_glory_app.wasm # Copy index.html into dist, then serve the directory over HTTP. python -m http.server 8080 --bind 127.0.0.1 --directory dist

不要通过 file:// 打开 index.html

ES 模块和 WASM 加载需要正确的 HTTP 源及 MIME 处理;浏览器 file:// 安全规则通常会阻止这些请求。本地开发也应通过 HTTP 提供生成目录。原生 cargo build 成功并不能验证浏览器启动路径。

通过 hydrate 连接 SSR 和浏览器

纯客户端 launch 创建新的 DOM。水合是独立的显式操作,它接管版本化 SSR Snapshot,不重放初始属性设置,也不替换现有子树。原生服务器构建启用 ssr,wasm32 浏览器构建启用 hydrate;hydrate 隐含 web。若两个目标共用一个依赖声明,features = ["web", "ssr", "hydrate"] 可选择完整配置。

将共享组件工厂放在两个目标都编译的代码中。服务器和浏览器通过相同应用逻辑构造等价的初始语义树。水合不会自动传递任意 Rust 堆状态;初始值必须一致,异步请求数据需要显式资源或应用数据策略。

src/app.rs

1 2 3 4 5 6
// Shared by the native SSR binary and the wasm32 browser crate. use glory::prelude::*; pub fn app() -> impl Widget { crate::Counter }

序列化 Snapshot

glory::ssr::snapshot(app()) 返回的 Snapshot,其 html 包含 hydrate 所需的版本标记。将片段原样插入应用独占的 #app 元素。还需以 data-gly-base="v1" 属性包含 glory::html::BASE_CSS;浏览器宿主便能识别该基础样式,避免重复添加。

src/server.rs

1 2 3 4 5 6 7 8 9 10 11 12 13 14
fn render_page() -> Result<String> { // snapshot(), rather than render(), includes hydration version markers. let snapshot = glory::ssr::snapshot(app())?; Ok(format!( "<!doctype html><html lang=\"en\"><head>\ <meta charset=\"utf-8\"><title>Counter</title>\ <style data-gly-base=\"v1\">{}</style></head><body>\ <main id=\"app\">{}</main>\ <script type=\"module\">import init from '/my_glory_app.js'; await init();</script>\ </body></html>", glory::html::BASE_CSS, snapshot.html, )) }

只需要 HTML 输出时,可使用 glory::ssr::render;但其未版本化字符串不满足水合约定。Snapshot 结构、组件工厂和浏览器特性必须一致。水合在附加运行时元数据前验证完整初始子树;版本或结构不匹配时报告错误,不会原地修复任意 DOM。

接管 DOM 并保留同一守卫

hydrate(root, widget) 和 hydrate_with(root, factory) 只用于 SSR 根元素。不要先对该根调用 launch:launch 要求为空,hydrate 则要求版本化服务器片段。浏览器仍会构造组件来建立状态和绑定,但会接管匹配的 DOM,不会重新创建。符合条件的已有用户输入会通过接管规则保留。

src/web.rs

1 2 3 4 5 6
// Enable Glory's hydrate feature for this browser build. let app = glory::web::hydrate(root, app()) .map_err(|error| JsValue::from_str(&error.to_string()))?; // Hydration also returns the WebApp guard that must be retained. APP.with(|slot| *slot.borrow_mut() = Some(app));

hydrate_with 工厂与 launch_with 一样接受 FnOnce(&Scope) -> W,适合共享工厂同时返回组件和浏览器应用状态所需句柄的场景。无论使用哪个入口,都要保留返回的 WebApp。水合改变初始 DOM 的获取方式,不改变运行应用的所有权。

回顾快速入门与宿主特性 →

完整启动约定

以 web 特性为 wasm32 构建;查找 web_sys 根元素;只对空 CSR 挂载元素使用 launch 或 launch_with;通过 HTTP 提供模块和 WASM;保留 WebApp;接管服务端 DOM 时,选择 hydrate 或 hydrate_with,并使用匹配的 SSR Snapshot。