服务器渲染与接管
SSR 与水合。
先在服务器端渲染有用的 HTML,再让浏览器接管完全一致的语义树。Glory 将静态输出、快照、资源传递和渐进式流式渲染作为明确的选项。
从普通 HTML 开始
片段使用 ssr::render,带有元数据、样式表和 BASE_CSS 的完整文档使用 ssr::render_document。这些 API 生成普通服务端 HTML,不生成版本化水合 Snapshot,因此没有可供 web::hydrate 接管的水合封装。
static_html.rs
use glory::prelude::*;
use glory::ssr::Document;
fn article() -> impl Widget {
col().fill((
text().kind(TextKind::Heading(1)).content("Glory SSR"),
text().content("Rendered by the semantic HTML mapper."),
))
}
// A plain HTML fragment: no versioned hydration envelope.
let fragment = glory::ssr::render(article())?;
// A complete document. BASE_CSS is included automatically.
let document = Document {
title: "Glory SSR".into(),
lang: Some("en".into()),
stylesheets: vec!["/assets/app.css".into()],
..Default::default()
};
let html = glory::ssr::render_document(article(), &document)?;普通 HTML 不能直接水合
静态页面选择 render 或 render_document。不要将其输出放入挂载元素后调用 hydrate:接管需要 snapshot、snapshot_with 或已完成的流式输出。
创建可接管的快照
snapshot 和 snapshot_with 为映射器输出添加 Glory 的版本化片段边界。将 Snapshot::html 原样插入一个除此之外为空、由应用独占的挂载元素。文档中需包含 glory::html::BASE_CSS;与 render_document 不同,手动组装快照不会自动添加它。
shared.rs
use glory::prelude::*;
// Share this factory and its initial inputs across server and browser.
fn counter(scope: &Scope, start: u64) -> impl Widget + use<> {
let count = scope.cage(start);
col().fill((
text().content(count.map(|value| format!("Count: {value}"))),
button()
.label("Increment")
.on(activate, move |_| count.revise(|value| *value += 1)),
))
}server.rs
// Native request handler: create a fresh snapshot for this request.
let snapshot = glory::ssr::snapshot_with(|scope| counter(scope, 0))?;
let html = 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\" src=\"/assets/bootstrap.js\"></script>\
</body></html>",
glory::html::BASE_CSS,
snapshot.html,
);
// Insert snapshot.html verbatim. Do not pretty-print or sanitize the markers.水合同一个应用
在 wasm32 浏览器宿主中,使用组件调用 web::hydrate,或使用工厂调用 web::hydrate_with。工厂、结构和初始数据必须与服务器一致。水合会在附加运行时状态前验证整个子树;版本或结构不匹配时返回 HydrationError,并保留现有 DOM。
browser.rs
use glory::web::WebApp;
fn mount(root: web_sys::Element) -> Result<WebApp, glory::web::HydrationError> {
// Same factory and same initial value as the server snapshot.
glory::web::hydrate_with(root, |scope| counter(scope, 0))
}
let app = mount(root)?;
// Store `app` for the entire application lifetime. Dropping WebApp releases
// the DOM binding, listeners, observers, frame host, and owned tasks.同步 SSR 不会等待异步工作
render、snapshot 及其 _with 形式会立即构建并序列化当前树,不会阻塞等待异步 Resource 加载器或待完成值。可显式注入已知终态数据、渲染等待状态,或在服务器需要逐步等待工作时使用 stream_with。
显式传递资源
可选 resources 特性只传递仍存活的具名终态值。为每个绑定指定稳定名称和 schema 字符串,并显式指定业务键。同步 SSR 可先通过 seed 将预取结果放入资源 Snapshot,恢复新的 Resources 上下文,再用 snapshot_resources_with 渲染,并在 App 清理移除注册前导出。
profile.rs
use glory::prelude::*;
fn profile(scope: &Scope, resources: &Resources) -> impl Widget + use<> {
let profile = resources
.bind(
scope.owner(),
"profile", // Stable wire name.
"profile/1", // Explicit schema version.
|| 7_u64, // Business key, not a global cache key.
|id| async move { Ok::<_, String>(load_profile(id).await) },
)
.expect("valid resource registration");
text().content(profile.map(|state| match state {
ResourceState::Ready { value, .. } => value.to_string(),
ResourceState::Failed { error, .. } => format!("Failed: {error}"),
_ => "Loading profile…".into(),
}))
}resource_server.rs
// Synchronous SSR needs an explicitly prefetched terminal value.
let mut seeds = glory::resources::Snapshot::default();
seeds.seed(
"profile",
"profile/1",
&7_u64,
Ok::<_, &String>(&"Ada Lovelace".to_owned()),
)?;
let resources = Resources::restore(seeds)?;
let output = glory::ssr::snapshot_resources_with(&resources, |scope| {
profile(scope, &resources)
})?;
let data = glory::ssr::resource_script("profile-data", &output.data, None)?;
let html = format!(
"<!doctype html><html><head><style data-gly-base=\"v1\">{}</style></head>\
<body><main id=\"app\">{}</main>{}</body></html>",
glory::html::BASE_CSS,
output.view.html,
data, // The inert JSON script is outside #app.
);resource_script 创建经过转义的惰性 application/json 数据块,不是可执行引导代码。应将其放在水合挂载元素之外。浏览器中,hydrate_resources_with 使用显式 Limits 读取数据,恢复 Resources,并运行相同具名绑定。匹配的成功或错误种子只会跳过首次请求。
resource_browser.rs
use glory::resources::{Limits, Resources};
use glory::web::{HydrationError, WebApp};
fn mount(
root: web_sys::Element,
data: web_sys::Element,
) -> Result<(WebApp, Resources), HydrationError> {
glory::web::hydrate_resources_with(
root,
&data,
Limits::default(),
|scope, resources| profile(scope, resources),
)
}
let (app, resources) = mount(root, data_element)?;
// Keep the WebApp alive. A matching seed suppresses only the first load;
// refresh and new business keys still invoke the loader normally.每个请求一个上下文
不要在请求或应用之间共享 Resources 上下文;克隆只共享当前上下文。每次都创建新的上下文。底层接管失败后,重试时应从原始载荷恢复新的上下文。hydrate_resources_with 在失败时保留数据块,仅在成功后移除。
流式输出 Shell、Patch 和 End
启用原生 stream 特性后,ssr::stream_with 返回本地、按需拉取的请求体。加载器轮询前即可获得 Shell;工作完成后 Patch 帧携带语义 HTML 编辑;End 携带终态资源和规范键重标记。在 CSP 允许的位置提供 web::STREAM_SCRIPT,并在 Shell 前输出 Bootstrap::head_script,以捕获早期输入。
stream_server.rs
use glory::prelude::*;
use glory::ssr::stream::{Bootstrap, Config};
// One local request context; the stream owns its App and async work.
let resources = Resources::default();
let mut body = glory::ssr::stream_with(
resources,
Config::default(),
|scope, resources| profile(scope, resources),
)?;
let bootstrap = Bootstrap {
root_id: "app".into(),
data_id: "stream-data".into(),
script_url: "/glory-stream.js".into(), // Serve web::STREAM_SCRIPT here.
nonce: Some(request_nonce),
};
// HTTP-host pseudocode: your adapter supplies send_head and send_chunk.
send_head(glory::html::BASE_CSS, &bootstrap.head_script()?).await?;
while let Some(frame) = body.next_frame().await {
let frame = frame?; // Shell, then zero or more Patch frames, then End.
send_chunk(frame.to_html(&bootstrap)?).await?;
// Awaiting the write before requesting another frame applies backpressure.
}
// Dropping `body` early cancels its request work.背压就是轮询约定:上一块写出后才请求下一帧。暂停需求会暂停渲染和加载器轮询;释放请求体会取消其 App 和 future。不要将其替换为缓存整个响应的预先生成器。
stream_browser.rs
use glory::resources::{Limits, Resources};
use glory::web::{HydrationError, WebApp};
fn adopt_stream(
root: web_sys::Element,
data: web_sys::Element,
) -> Result<(WebApp, Resources), HydrationError> {
glory::web::hydrate_resources_with(
root,
&data,
Limits::default(),
|scope, resources| profile(scope, resources),
)
}
// Call adopt_stream only from the root's `glory:stream-ready` listener.
// That event follows End, final key relabeling, and resource handoff.
let (app, resources) = adopt_stream(root, stream_data)?;
// Retain `app` just as you would after ordinary hydration.等待最终状态
流式 Shell 使用临时流标记,刻意不支持接管。不要对 Shell 或中途 Patch 执行水合。End 应用后,根节点会发出 glory:stream-ready;此时才可使用相同工厂和传递的资源水合最终树。
选择渲染路径
需求
服务器 API
浏览器结果
静态内容
render / render_document
普通 HTML;无需接管
可交互的初始树
snapshot / snapshot_with
hydrate / hydrate_with
预取的具名数据
snapshot_resources_with + resource_script
hydrate_resources_with
渐进式异步工作
stream_with + Bootstrap
仅在 End / glory:stream-ready 后水合
常见问题
重新构建而非接管
launch 需要空的挂载元素并创建新树。Snapshot 应使用 hydrate;不要先清空或重写服务端标记。
初始输入发生变化
工厂、初始 Cage 值、资源名、schema 或最终资源状态不同,都可能导致不匹配或额外加载。应共享组件和种子约定。
释放了 WebApp
返回的 WebApp 是生命周期守卫,不能当作一次性结果丢弃。只要界面需要保持挂载和交互,就应保存它。
期待隐式异步 SSR
同步快照不会等待 future。已有数据使用显式种子,需要渐进完成则使用按需拉取的流。
将数据块嵌入挂载元素
将 resource_script 输出放在挂载元素之外。水合拥有该元素的完整子树;独立 JSON 块有自己的标识和生命周期。
自行接入生产 HTTP 服务器
Glory 提供渲染、帧和浏览器引导资源,不提供生产 HTTP 服务器。路由、TLS、并发、套接字写入、CSP、断连处理和部署由宿主负责。semantic-web 中的 ssr_fixture 和 stream_fixture 二进制程序是验收工具,不是 Axum 或 Salvo 集成,也不是生产服务器实现。