服务器渲染与接管

SSR 与水合。

先在服务器端渲染有用的 HTML,再让浏览器接管完全一致的语义树。Glory 将静态输出、快照、资源传递和渐进式流式渲染作为明确的选项。

从普通 HTML 开始

片段使用 ssr::render,带有元数据、样式表和 BASE_CSS 的完整文档使用 ssr::render_document。这些 API 生成普通服务端 HTML,不生成版本化水合 Snapshot,因此没有可供 web::hydrate 接管的水合封装。

static_html.rs

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
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

1 2 3 4 5 6 7 8 9 10 11 12
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

1 2 3 4 5 6 7 8 9 10 11 12 13
// 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

1 2 3 4 5 6 7 8 9 10
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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// 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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
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 集成,也不是生产服务器实现。