Introduce additional information in SSR artifact to facilitate Hydration (#2540)

* Bring changes to this branch.

* Child components always render after parents.

* Add hydratable to render_to_string.

* Revert portal example.

* Fix vcomp.

* Prefer debug_assert.

* ServerRenderer now a Builder Pattern.

* Collectable.
This commit is contained in:
Kaede Hoshikawa 2022-03-26 08:57:08 +09:00 committed by GitHub
parent ee6a67e3ea
commit 2e098f4f6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 246 additions and 51 deletions

View File

@ -204,8 +204,15 @@ mod feat_ssr {
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
};
use crate::virtual_dom::Collectable;
impl<COMP: BaseComponent> Scope<COMP> {
pub(crate) async fn render_to_string(self, w: &mut String, props: Rc<COMP::Properties>) {
pub(crate) async fn render_to_string(
self,
w: &mut String,
props: Rc<COMP::Properties>,
hydratable: bool,
) {
let (tx, rx) = oneshot::channel();
let state = ComponentRenderState::Ssr { sender: Some(tx) };
@ -222,10 +229,24 @@ mod feat_ssr {
);
scheduler::start();
#[cfg(debug_assertions)]
let collectable = Collectable::Component(std::any::type_name::<COMP>());
#[cfg(not(debug_assertions))]
let collectable = Collectable::Component;
if hydratable {
collectable.write_open_tag(w);
}
let html = rx.await.unwrap();
let self_any_scope = AnyScope::from(self.clone());
html.render_to_string(w, &self_any_scope).await;
html.render_to_string(w, &self_any_scope, hydratable).await;
if hydratable {
collectable.write_close_tag(w);
}
scheduler::push_component_destroy(Box::new(DestroyRunner {
state: self.state.clone(),

View File

@ -10,6 +10,7 @@ where
ICOMP: IntoComponent,
{
props: ICOMP::Properties,
hydratable: bool,
}
impl<ICOMP> Default for ServerRenderer<ICOMP>
@ -39,7 +40,22 @@ where
{
/// Creates a [ServerRenderer] with custom properties.
pub fn with_props(props: ICOMP::Properties) -> Self {
Self { props }
Self {
props,
hydratable: true,
}
}
/// Sets whether an the rendered result is hydratable.
///
/// Defaults to `true`.
///
/// When this is sets to `true`, the rendered artifact will include additional information
/// to assist with the hydration process.
pub fn hydratable(mut self, val: bool) -> Self {
self.hydratable = val;
self
}
/// Renders Yew Application.
@ -54,6 +70,8 @@ where
/// Renders Yew Application to a String.
pub async fn render_to_string(self, w: &mut String) {
let scope = Scope::<<ICOMP as IntoComponent>::Component>::new(None);
scope.render_to_string(w, self.props.into()).await;
scope
.render_to_string(w, self.props.into(), self.hydratable)
.await;
}
}

View File

@ -179,6 +179,84 @@ mod tests_attr_value {
}
}
#[cfg(feature = "ssr")] // & feature = "hydration"
mod feat_ssr_hydration {
/// A collectable.
///
/// This indicates a kind that can be collected from fragment to be processed at a later time
pub(crate) enum Collectable {
#[cfg(debug_assertions)]
Component(&'static str),
#[cfg(not(debug_assertions))]
Component,
Suspense,
}
impl Collectable {
pub fn open_start_mark(&self) -> &'static str {
match self {
#[cfg(debug_assertions)]
Self::Component(_) => "<[",
#[cfg(not(debug_assertions))]
Self::Component => "<[",
Self::Suspense => "<?",
}
}
pub fn close_start_mark(&self) -> &'static str {
match self {
#[cfg(debug_assertions)]
Self::Component(_) => "</[",
#[cfg(not(debug_assertions))]
Self::Component => "</[",
Self::Suspense => "</?",
}
}
pub fn end_mark(&self) -> &'static str {
match self {
#[cfg(debug_assertions)]
Self::Component(_) => "]>",
#[cfg(not(debug_assertions))]
Self::Component => "]>",
Self::Suspense => ">",
}
}
#[cfg(feature = "ssr")]
pub fn write_open_tag(&self, w: &mut String) {
w.push_str("<!--");
w.push_str(self.open_start_mark());
#[cfg(debug_assertions)]
match self {
Self::Component(type_name) => w.push_str(type_name),
Self::Suspense => {}
}
w.push_str(self.end_mark());
w.push_str("-->");
}
#[cfg(feature = "ssr")]
pub fn write_close_tag(&self, w: &mut String) {
w.push_str("<!--");
w.push_str(self.close_start_mark());
#[cfg(debug_assertions)]
match self {
Self::Component(type_name) => w.push_str(type_name),
Self::Suspense => {}
}
w.push_str(self.end_mark());
w.push_str("-->");
}
}
}
#[cfg(feature = "ssr")]
pub(crate) use feat_ssr_hydration::*;
/// A collection of attributes for an element
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Attributes {

View File

@ -70,6 +70,7 @@ pub(crate) trait Mountable {
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
hydratable: bool,
) -> LocalBoxFuture<'a, ()>;
}
@ -117,10 +118,13 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
hydratable: bool,
) -> LocalBoxFuture<'a, ()> {
async move {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
scope.render_to_string(w, self.props.clone()).await;
scope
.render_to_string(w, self.props.clone(), hydratable)
.await;
}
.boxed_local()
}
@ -210,10 +214,15 @@ mod feat_ssr {
use crate::html::AnyScope;
impl VComp {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
pub(crate) async fn render_to_string(
&self,
w: &mut String,
parent_scope: &AnyScope,
hydratable: bool,
) {
self.mountable
.as_ref()
.render_to_string(w, parent_scope)
.render_to_string(w, parent_scope, hydratable)
.await;
}
}

View File

@ -90,12 +90,17 @@ mod feat_ssr {
use crate::html::AnyScope;
impl VList {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
pub(crate) async fn render_to_string(
&self,
w: &mut String,
parent_scope: &AnyScope,
hydratable: bool,
) {
// Concurrently render all children.
for fragment in futures::future::join_all(self.children.iter().map(|m| async move {
let mut w = String::new();
m.render_to_string(&mut w, parent_scope).await;
m.render_to_string(&mut w, parent_scope, hydratable).await;
w
}))
@ -123,9 +128,10 @@ mod ssr_tests {
html! { <div>{"Hello "}{s}{"!"}</div> }
}
let renderer = ServerRenderer::<Comp>::new();
let s = renderer.render().await;
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, "<div>Hello world!</div>");
}
@ -153,9 +159,10 @@ mod ssr_tests {
}
}
let renderer = ServerRenderer::<Comp>::new();
let s = renderer.render().await;
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(
s,

View File

@ -157,13 +157,20 @@ mod feat_ssr {
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
hydratable: bool,
) -> LocalBoxFuture<'a, ()> {
async move {
match self {
VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope).await,
VNode::VText(vtext) => vtext.render_to_string(w).await,
VNode::VComp(vcomp) => vcomp.render_to_string(w, parent_scope).await,
VNode::VList(vlist) => vlist.render_to_string(w, parent_scope).await,
VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope, hydratable).await,
VNode::VText(vtext) => {
vtext.render_to_string(w, parent_scope, hydratable).await
}
VNode::VComp(vcomp) => {
vcomp.render_to_string(w, parent_scope, hydratable).await
}
VNode::VList(vlist) => {
vlist.render_to_string(w, parent_scope, hydratable).await
}
// We are pretty safe here as it's not possible to get a web_sys::Node without DOM
// support in the first place.
//
@ -175,7 +182,9 @@ mod feat_ssr {
// Portals are not rendered.
VNode::VPortal(_) => {}
VNode::VSuspense(vsuspense) => {
vsuspense.render_to_string(w, parent_scope).await
vsuspense
.render_to_string(w, parent_scope, hydratable)
.await
}
}
}

View File

@ -28,11 +28,29 @@ impl VSuspense {
mod feat_ssr {
use super::*;
use crate::html::AnyScope;
use crate::virtual_dom::Collectable;
impl VSuspense {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
pub(crate) async fn render_to_string(
&self,
w: &mut String,
parent_scope: &AnyScope,
hydratable: bool,
) {
let collectable = Collectable::Suspense;
if hydratable {
collectable.write_open_tag(w);
}
// always render children on the server side.
self.children.render_to_string(w, parent_scope).await;
self.children
.render_to_string(w, parent_scope, hydratable)
.await;
if hydratable {
collectable.write_close_tag(w);
}
}
}
}
@ -120,9 +138,10 @@ mod ssr_tests {
let s = local
.run_until(async move {
let renderer = ServerRenderer::<Comp>::new();
renderer.render().await
ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await
})
.await;

View File

@ -418,8 +418,19 @@ mod feat_ssr {
use crate::{html::AnyScope, virtual_dom::VText};
use std::fmt::Write;
// Elements that cannot have any child elements.
static VOID_ELEMENTS: &[&str; 14] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
"source", "track", "wbr",
];
impl VTag {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
pub(crate) async fn render_to_string(
&self,
w: &mut String,
parent_scope: &AnyScope,
hydratable: bool,
) {
write!(w, "<{}", self.tag()).unwrap();
let write_attr = |w: &mut String, name: &str, val: Option<&str>| {
@ -450,7 +461,9 @@ mod feat_ssr {
VTagInner::Input(_) => {}
VTagInner::Textarea { .. } => {
if let Some(m) = self.value() {
VText::new(m.to_owned()).render_to_string(w).await;
VText::new(m.to_owned())
.render_to_string(w, parent_scope, hydratable)
.await;
}
w.push_str("</textarea>");
@ -460,9 +473,14 @@ mod feat_ssr {
ref children,
..
} => {
children.render_to_string(w, parent_scope).await;
if !VOID_ELEMENTS.contains(&tag.as_ref()) {
children.render_to_string(w, parent_scope, hydratable).await;
write!(w, "</{}>", tag).unwrap();
write!(w, "</{}>", tag).unwrap();
} else {
// We don't write children of void elements nor closing tags.
debug_assert!(children.is_empty(), "{} cannot have any children!", tag);
}
}
}
}
@ -483,9 +501,10 @@ mod ssr_tests {
html! { <div></div> }
}
let renderer = ServerRenderer::<Comp>::new();
let s = renderer.render().await;
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, "<div></div>");
}
@ -497,9 +516,10 @@ mod ssr_tests {
html! { <div class="abc"></div> }
}
let renderer = ServerRenderer::<Comp>::new();
let s = renderer.render().await;
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<div class="abc"></div>"#);
}
@ -511,9 +531,10 @@ mod ssr_tests {
html! { <div>{"Hello!"}</div> }
}
let renderer = ServerRenderer::<Comp>::new();
let s = renderer.render().await;
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<div>Hello!</div>"#);
}
@ -525,9 +546,10 @@ mod ssr_tests {
html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
}
let renderer = ServerRenderer::<Comp>::new();
let s = renderer.render().await;
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
}
@ -539,9 +561,10 @@ mod ssr_tests {
html! { <textarea value="teststring" /> }
}
let renderer = ServerRenderer::<Comp>::new();
let s = renderer.render().await;
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
}

View File

@ -34,9 +34,15 @@ impl PartialEq for VText {
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
use crate::html::AnyScope;
impl VText {
pub(crate) async fn render_to_string(&self, w: &mut String) {
pub(crate) async fn render_to_string(
&self,
w: &mut String,
_parent_scope: &AnyScope,
_hydratable: bool,
) {
html_escape::encode_text_to_string(&self.text, w);
}
}
@ -46,16 +52,21 @@ mod feat_ssr {
mod ssr_tests {
use tokio::test;
use super::*;
use crate::prelude::*;
use crate::ServerRenderer;
#[test]
async fn test_simple_str() {
let vtext = VText::new("abc");
#[function_component]
fn Comp() -> Html {
html! { "abc" }
}
let mut s = String::new();
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
vtext.render_to_string(&mut s).await;
assert_eq!("abc", s.as_str());
assert_eq!(s, r#"abc"#);
}
}