From 2e098f4f6c7a12efde10957ca773f5f592c65c93 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 08:57:08 +0900 Subject: [PATCH] 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. --- packages/yew/src/html/component/scope.rs | 25 +++++++- packages/yew/src/server_renderer.rs | 22 ++++++- packages/yew/src/virtual_dom/mod.rs | 78 +++++++++++++++++++++++ packages/yew/src/virtual_dom/vcomp.rs | 15 ++++- packages/yew/src/virtual_dom/vlist.rs | 23 ++++--- packages/yew/src/virtual_dom/vnode.rs | 19 ++++-- packages/yew/src/virtual_dom/vsuspense.rs | 29 +++++++-- packages/yew/src/virtual_dom/vtag.rs | 61 ++++++++++++------ packages/yew/src/virtual_dom/vtext.rs | 25 ++++++-- 9 files changed, 246 insertions(+), 51 deletions(-) diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index f48168f9e..7cae98cec 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -204,8 +204,15 @@ mod feat_ssr { ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, }; + use crate::virtual_dom::Collectable; + impl Scope { - pub(crate) async fn render_to_string(self, w: &mut String, props: Rc) { + pub(crate) async fn render_to_string( + self, + w: &mut String, + props: Rc, + 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::()); + + #[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(), diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index b72773406..0a825da18 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -10,6 +10,7 @@ where ICOMP: IntoComponent, { props: ICOMP::Properties, + hydratable: bool, } impl Default for ServerRenderer @@ -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::<::Component>::new(None); - scope.render_to_string(w, self.props.into()).await; + scope + .render_to_string(w, self.props.into(), self.hydratable) + .await; } } diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 80e85ecaa..2970eae98 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -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 => " &'static str { + match self { + #[cfg(debug_assertions)] + Self::Component(_) => " " " &'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(""); + } + + #[cfg(feature = "ssr")] + pub fn write_close_tag(&self, w: &mut String) { + 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 { diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 7912238a0..e380ac506 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -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 Mountable for PropsWrapper { &'a self, w: &'a mut String, parent_scope: &'a AnyScope, + hydratable: bool, ) -> LocalBoxFuture<'a, ()> { async move { let scope: Scope = 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; } } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 9304f00d2..3471636c7 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -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! {
{"Hello "}{s}{"!"}
} } - let renderer = ServerRenderer::::new(); - - let s = renderer.render().await; + let s = ServerRenderer::::new() + .hydratable(false) + .render() + .await; assert_eq!(s, "
Hello world!
"); } @@ -153,9 +159,10 @@ mod ssr_tests { } } - let renderer = ServerRenderer::::new(); - - let s = renderer.render().await; + let s = ServerRenderer::::new() + .hydratable(false) + .render() + .await; assert_eq!( s, diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 975f3cb84..72ce6fafa 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -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 } } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index e9448c5aa..8d43ad360 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -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::::new(); - - renderer.render().await + ServerRenderer::::new() + .hydratable(false) + .render() + .await }) .await; diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index ed9debd0d..eb064c5d1 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -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(""); @@ -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! {
} } - let renderer = ServerRenderer::::new(); - - let s = renderer.render().await; + let s = ServerRenderer::::new() + .hydratable(false) + .render() + .await; assert_eq!(s, "
"); } @@ -497,9 +516,10 @@ mod ssr_tests { html! {
} } - let renderer = ServerRenderer::::new(); - - let s = renderer.render().await; + let s = ServerRenderer::::new() + .hydratable(false) + .render() + .await; assert_eq!(s, r#"
"#); } @@ -511,9 +531,10 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = ServerRenderer::::new(); - - let s = renderer.render().await; + let s = ServerRenderer::::new() + .hydratable(false) + .render() + .await; assert_eq!(s, r#"
Hello!
"#); } @@ -525,9 +546,10 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = ServerRenderer::::new(); - - let s = renderer.render().await; + let s = ServerRenderer::::new() + .hydratable(false) + .render() + .await; assert_eq!(s, r#"
Hello!
"#); } @@ -539,9 +561,10 @@ mod ssr_tests { html! { "#); } diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 1756e0bbd..09b039282 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -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::::new() + .hydratable(false) + .render() + .await; - vtext.render_to_string(&mut s).await; - - assert_eq!("abc", s.as_str()); + assert_eq!(s, r#"abc"#); } }