maomi
介绍
入门
文档

服务器端渲染

服务器端渲染是如何工作的

普通情况下, maomi 在浏览器中生成 HTML 节点。在初始化时,它从一个空的 DOM 节点(即挂载点)开始,不断向其中插入和修改子孙节点。

服务器端渲染可以在服务器上生成 HTML 片段。这个 HTML 片段可以在初始化前被插入到挂载点中,然后 maomi 会复用这些 HTML 节点。

这使得静态内容可以直接内嵌到 HTML 中,更利于搜索引擎理解。

因为 maomi 是 rust 编写的,组件代码可以被编译为原生代码、直接用于生成 HTML 。这意味着组件会被编译两次:一次是编译为原生代码,另一次是编译为在浏览器中运行的 WebAssembly 代码。

使用服务器端渲染前,需要在 Cargo.toml 中指定额外的 feature 。激活 prerendering 可以启用服务器端支持,激活 prerendering-apply 则可以在客户端复用服务器端生成的 HTML 节点。

[dependencies]
maomi = { version = "0.4", features = ["prerendering", "prerendering-apply"] }
maomi-dom = { version = "0.4", features = ["prerendering", "prerendering-apply"] }
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
bincode = "1.3"

请注意激活这些 feature 会带来一点点运行时开销。如果不用服务器端渲染就不要激活它们。

预渲染组件

为了能在服务器端渲染,组件必须实现 PrerenderableComponent 。这个 trait 有两个关联类型和两个关联函数。

QueryData 代表请求参数的类型,比如 URL 参数、 POST 数据或者其他相关数据。对于静态内容,可以只是一个空类型 () 。

PrerenderingData 代表预渲染数据的类型。预渲染数据由 QueryData 转化得到,包含 QueryData 中有用的信息数据。它应当能被序列化并从服务器端传到客户端。

服务器端渲染时,首先, prerendering_data 函数被调用,用于将 QueryData 转换为 PrerenderingData 。(当进行客户端渲染时,它也可能在客户端被调用。)

第二,组件在服务器端被创建, apply_prerendering_data 被调用。这个函数根据 PrerenderingData 来更新组件数据字段。组件创建完成后, HTML 片段就可以生成了。

第三,生成的 HTML 需要被嵌入到挂载点中, PrerenderingData 也要被传递到客户端。

第四,组件在客户端启动, apply_prerendering_data 同样被调用。这个函数的执行必须与在服务器端的执行逻辑相同。这样组件就可以复用服务器端生成的 HTML 节点了。

#[component(Backend = DomBackend)]
struct MyWebsite {
template: template! {
<div> "My Website" </div>
},
content: String,
}
impl Component for MyWebsite {
fn new() -> Self {
Self {
template: Default::default(),
content: String::new(),
}
}
}
#[async_trait::async_trait]
impl PrerenderableComponent for MyWebsite {
type QueryData = ();
type PrerenderingData = ();
async fn prerendering_data(_: &Self::QueryData) -> Self::PrerenderingData {
()
}
fn apply_prerendering_data(&mut self, _: Self::PrerenderingData) {
self.content = "Prerendering String".to_string();
}
}

请注意激活这些 feature 会带来一点点运行时开销。如果不用服务器端渲染就不要激活它们。

在服务器端生成 HTML

在服务器端生成 HTML 需要用到特殊的 backend 环境

use maomi::template::ComponentTemplate;
async fn prerendering<C: PrerenderableComponent + ComponentTemplate<DomBackend>>(
query_data: &C::QueryData,
) -> (Vec<u8>, Vec<u8>)
where
C::PrerenderingData: serde::Serialize,
{
// 生成 PrerenderingData
let prerendering_data = maomi::BackendContext::<DomBackend>::prerendering_data::<C>(&query_data).await;
// 序列化 PrerenderingData 并传递给客户端
let prerendering_data_bin = bincode::serialize(prerendering_data.get()).unwrap();
// 初始化一个 backend 环境用于 HTML 生成
let prerendering_dom_backend = DomBackend::prerendering();
let backend_context = maomi::BackendContext::new(prerendering_dom_backend);
// 生成 HTML 片段
let (_mount_point, html_buffer) = backend_context
.enter_sync(move |ctx| {
let mount_point = ctx.prerendering_attach(prerendering_data).unwrap();
let mut html_buffer = vec![];
ctx.write_prerendering_html(&mut html_buffer).unwrap();
(mount_point, html_buffer)
})
.map_err(|_| "Cannot init mount point")
.unwrap();
// 返回 HTML 片段和 PrerenderingData
// 生成好的 HTML 需要最终放置在 HTML <body /> 内
// PrerenderingData 可以被 base64 编码后放入 HTML 中,或者以其他形式传输到客户端
(html_buffer, prerendering_data_bin)
}

在客户端复用生成的 HTML 节点

为了复用生成好的 HTML , backend 环境需要使用 PrerenderingData 来初始化。

use maomi::template::ComponentTemplate;
fn prerendering_apply<C: PrerenderableComponent + ComponentTemplate<DomBackend>>(
prerendering_data_bin: Vec<u8>,
)
where
for<'de> C::PrerenderingData: serde::Deserialize<'de>,
{
// 反序列化 PrerenderingData
let data = bincode::deserialize(&prerendering_data_bin).unwrap();
// 构建 PrerenderingData
let prerendering_data = maomi::PrerenderingData::<C>::new(data);
// 初始化 backend 环境,用于复用生成好的 HTML
let dom_backend = DomBackend::new_prerendered();
let backend_context = maomi::BackendContext::new(dom_backend);
// 创建挂载点
let mount_point = backend_context
.enter_sync(move |ctx| {
let mount_point = ctx.prerendering_attach(prerendering_data).unwrap();
ctx.apply_prerendered_document_body().unwrap();
mount_point
})
.map_err(|_| "Cannot init mount point")
.unwrap();
// 将挂载点和 backend 环境泄露掉、使它们不在函数结束时回收,这样就可以一直运行
std::mem::forget(mount_point);
std::mem::forget(backend_context);
}