From 65af3a24df69b77af563c915c60f7b335792a874 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 6 Jan 2022 19:31:30 +0900 Subject: [PATCH 01/27] Basic render to html implementation. --- packages/yew/Cargo.toml | 2 + packages/yew/src/html/component/lifecycle.rs | 62 +++++++--- packages/yew/src/html/component/scope.rs | 45 ++++++- packages/yew/src/html_writer.rs | 22 ++++ packages/yew/src/lib.rs | 120 +++++++++++++++++++ packages/yew/src/virtual_dom/vcomp.rs | 46 ++++++- packages/yew/src/virtual_dom/vlist.rs | 13 ++ packages/yew/src/virtual_dom/vnode.rs | 36 ++++++ packages/yew/src/virtual_dom/vsuspense.rs | 12 ++ packages/yew/src/virtual_dom/vtag.rs | 38 ++++++ packages/yew/src/virtual_dom/vtext.rs | 12 ++ 11 files changed, 384 insertions(+), 24 deletions(-) create mode 100644 packages/yew/src/html_writer.rs diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 9d30ad38267..d14d596739b 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -26,6 +26,8 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" yew-macro = { version = "^0.19.0", path = "../yew-macro" } thiserror = "1.0" +async-trait = "0.1" +futures = "0.3" scoped-tls-hkt = "0.1" diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index e1b7a5e4108..60d4d1cb3a2 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -7,6 +7,7 @@ use crate::suspense::{Suspense, Suspension}; use crate::virtual_dom::{VDiff, VNode}; use crate::Callback; use crate::{Context, NodeRef}; +use futures::channel::oneshot; use std::rc::Rc; use web_sys::Element; @@ -15,13 +16,18 @@ pub(crate) struct ComponentState { pub(crate) root_node: VNode, context: Context, - parent: Element, + + /// When a component has no parent, it means that it should not be rendered. + parent: Option, + next_sibling: NodeRef, node_ref: NodeRef, has_rendered: bool, suspension: Option, + html_sender: Option>, + // Used for debug logging #[cfg(debug_assertions)] pub(crate) vcomp_id: u64, @@ -29,12 +35,13 @@ pub(crate) struct ComponentState { impl ComponentState { pub(crate) fn new( - parent: Element, + parent: Option, next_sibling: NodeRef, root_node: VNode, node_ref: NodeRef, scope: Scope, props: Rc, + html_sender: Option>, ) -> Self { #[cfg(debug_assertions)] let vcomp_id = { @@ -55,6 +62,8 @@ impl ComponentState { suspension: None, has_rendered: false, + html_sender, + #[cfg(debug_assertions)] vcomp_id, } @@ -62,12 +71,13 @@ impl ComponentState { } pub(crate) struct CreateRunner { - pub(crate) parent: Element, + pub(crate) parent: Option, pub(crate) next_sibling: NodeRef, pub(crate) placeholder: VNode, pub(crate) node_ref: NodeRef, pub(crate) props: Rc, pub(crate) scope: Scope, + pub(crate) html_sender: Option>, } impl Runnable for CreateRunner { @@ -84,6 +94,7 @@ impl Runnable for CreateRunner { self.node_ref, self.scope.clone(), self.props, + self.html_sender, )); } } @@ -129,11 +140,13 @@ impl Runnable for UpdateRunner { } } UpdateEvent::Shift(parent, next_sibling) => { - state - .root_node - .shift(&state.parent, &parent, next_sibling.clone()); + state.root_node.shift( + state.parent.as_ref().unwrap(), + &parent, + next_sibling.clone(), + ); - state.parent = parent; + state.parent = Some(parent); state.next_sibling = next_sibling; false @@ -173,8 +186,11 @@ impl Runnable for DestroyRunner { crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy"); state.component.destroy(&state.context); - state.root_node.detach(&state.parent); - state.node_ref.set(None); + + if let Some(ref m) = state.parent { + state.root_node.detach(m); + state.node_ref.set(None); + } } } } @@ -194,7 +210,9 @@ impl Runnable for RenderRunner { // Currently not suspended, we remove any previous suspension and update // normally. let mut root = m; - std::mem::swap(&mut root, &mut state.root_node); + if state.parent.is_some() { + std::mem::swap(&mut root, &mut state.root_node); + } if let Some(ref m) = state.suspension { let comp_scope = AnyScope::from(state.context.scope.clone()); @@ -205,13 +223,17 @@ impl Runnable for RenderRunner { suspense.resume(m.clone()); } - let ancestor = Some(root); - let new_root = &mut state.root_node; - let scope = state.context.scope.clone().into(); - let next_sibling = state.next_sibling.clone(); + if let Some(ref m) = state.parent { + let ancestor = Some(root); + let new_root = &mut state.root_node; + let scope = state.context.scope.clone().into(); + let next_sibling = state.next_sibling.clone(); - let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor); - state.node_ref.link(node); + let node = new_root.apply(&scope, m, next_sibling, ancestor); + state.node_ref.link(node); + } else if let Some(tx) = state.html_sender.take() { + tx.send(root).unwrap(); + } } Err(RenderError::Suspended(m)) => { @@ -277,9 +299,11 @@ impl Runnable for RenderedRunner { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered"); - let first_render = !state.has_rendered; - state.component.rendered(&state.context, first_render); - state.has_rendered = true; + if state.parent.is_some() { + let first_render = !state.has_rendered; + state.component.rendered(&state.context, first_render); + state.has_rendered = true; + } } } } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 410f9c34530..90d73263363 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -12,6 +12,7 @@ use crate::context::{ContextHandle, ContextProvider}; use crate::html::NodeRef; use crate::scheduler::{self, Shared}; use crate::virtual_dom::{insert_node, VNode}; +use futures::channel::oneshot; use gloo_utils::document; use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell}; @@ -234,12 +235,13 @@ impl Scope { scheduler::push_component_create( CreateRunner { - parent, + parent: Some(parent), next_sibling, placeholder, node_ref, props, scope: self.clone(), + html_sender: None, }, RenderRunner { state: self.state.clone(), @@ -414,6 +416,47 @@ impl Scope { } } +mod feat_ssr { + use super::*; + use crate::html_writer::HtmlWriter; + + impl Scope { + /// Renders Into a [`HtmlWrite`]. + pub(crate) async fn render_to_html(&self, w: &HtmlWriter, props: Rc) { + let (tx, rx) = oneshot::channel(); + + scheduler::push_component_create( + CreateRunner { + parent: None, + next_sibling: NodeRef::default(), + placeholder: VNode::default(), + node_ref: NodeRef::default(), + props, + scope: self.clone(), + html_sender: Some(tx), + }, + RenderRunner { + state: self.state.clone(), + }, + RenderedRunner { + state: self.state.clone(), + }, + ); + scheduler::start(); + + let html = rx.await.unwrap(); + + let self_any_scope = self.to_any(); + html.render_to_html(w, &self_any_scope).await; + + scheduler::push_component_destroy(DestroyRunner { + state: self.state.clone(), + }); + scheduler::start(); + } + } +} + /// Defines a message type that can be sent to a component. /// Used for the return value of closure given to [Scope::batch_callback](struct.Scope.html#method.batch_callback). pub trait SendAsMessage { diff --git a/packages/yew/src/html_writer.rs b/packages/yew/src/html_writer.rs new file mode 100644 index 00000000000..4122d48adf3 --- /dev/null +++ b/packages/yew/src/html_writer.rs @@ -0,0 +1,22 @@ +//! module to provide an html writer that can be written without mutable borrowing. + +mod feat_ssr { + use std::cell::RefCell; + + #[derive(Debug, Default)] + pub(crate) struct HtmlWriter { + inner: RefCell, + } + + impl HtmlWriter { + pub fn push_str(&self, s: &str) { + self.inner.borrow_mut().push_str(s); + } + + pub fn into_inner(self) -> String { + self.inner.into_inner() + } + } +} + +pub use feat_ssr::*; diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index d988d2eccdf..1a33af8559d 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -260,6 +260,7 @@ pub mod callback; pub mod context; pub mod functional; pub mod html; +mod html_writer; pub mod scheduler; mod sealed; pub mod suspense; @@ -304,6 +305,125 @@ fn set_default_panic_hook() { } } +// /// A Yew Renderer. +// #[derive(Debug)] +// pub struct YewRenderer +// where +// COMP: BaseComponent, +// { +// root: Element, +// props: COMP::Properties, +// } + +// impl YewRenderer +// where +// COMP: BaseComponent, +// { +// /// Creates a [`YewRenderer`] with a custom root and properties. +// pub fn with_root_and_props(root: Element, props: COMP::Properties) -> Self { +// Self { root, props } +// } + +// /// Creates a [`YewRenderer`] with document body as root and custom properties. +// pub fn with_props(props: COMP::Properties) -> Self { +// Self::with_root_and_props( +// gloo_utils::document() +// .body() +// .expect("no body node found") +// .into(), +// props, +// ) +// } + +// /// Renders a Yew application. +// pub fn render(self) -> AppHandle { +// set_default_panic_hook(); + +// AppHandle::::mount_with_props(self.root, Rc::new(self.props)) +// } + +// /// Hydrates a Yew application. +// pub fn hydrate(self) -> AppHandle { +// set_default_panic_hook(); +// todo!() +// } +// } + +// impl YewRenderer +// where +// COMP: BaseComponent, +// COMP::Properties: Default, +// { +// /// Creates a [`YewRenderer`] with a custom root. +// pub fn with_root(root: Element) -> Self { +// Self::with_root_and_props(root, COMP::Properties::default()) +// } + +// /// Creates a [`YewRenderer`] with document body as root. +// pub fn body() -> Self { +// Self::with_props(COMP::Properties::default()) +// } +// } + +mod feat_ssr { + use super::*; + + use crate::html::Scope; + use crate::html_writer::HtmlWriter; + + /// A Yew Server-side Renderer. + #[derive(Debug)] + pub struct YewServerRenderer + where + COMP: BaseComponent, + { + props: COMP::Properties, + } + + impl Default for YewServerRenderer + where + COMP: BaseComponent, + COMP::Properties: Default, + { + fn default() -> Self { + Self::with_props(COMP::Properties::default()) + } + } + + impl YewServerRenderer + where + COMP: BaseComponent, + COMP::Properties: Default, + { + /// Creates a [`YewServerRenderer`] with default properties. + pub fn new() -> Self { + Self::default() + } + } + + impl YewServerRenderer + where + COMP: BaseComponent, + { + /// Creates a [`YewServerRenderer`] with custom properties. + pub fn with_props(props: COMP::Properties) -> Self { + Self { props } + } + + /// Renders Yew Application into a string. + pub async fn render_to_string(self) -> String { + let s = HtmlWriter::default(); + + let scope = Scope::::new(None); + scope.render_to_html(&s, self.props.into()).await; + + s.into_inner() + } + } +} + +pub use feat_ssr::*; + /// The main entry point of a Yew application. /// If you would like to pass props, use the `start_app_with_props_in_element` method. pub fn start_app_in_element(element: Element) -> AppHandle diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index a0055b20697..71fb0e2b1df 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -2,6 +2,8 @@ use super::{Key, VDiff, VNode}; use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped}; +use crate::html_writer::HtmlWriter; +use futures::future::{FutureExt, LocalBoxFuture}; use std::any::TypeId; use std::borrow::Borrow; use std::fmt; @@ -41,7 +43,7 @@ pub(crate) fn get_event_log(vcomp_id: u64) -> Vec { pub struct VComp { type_id: TypeId, scope: Option>, - props: Option>, + mountable: Option>, pub(crate) node_ref: NodeRef, pub(crate) key: Option, @@ -62,7 +64,7 @@ impl Clone for VComp { Self { type_id: self.type_id, scope: None, - props: self.props.as_ref().map(|m| m.copy()), + mountable: self.mountable.as_ref().map(|m| m.copy()), node_ref: self.node_ref.clone(), key: self.key.clone(), @@ -132,7 +134,7 @@ impl VComp { VComp { type_id: TypeId::of::(), node_ref, - props: Some(Box::new(PropsWrapper::::new(props))), + mountable: Some(Box::new(PropsWrapper::::new(props))), scope: None, key, @@ -181,6 +183,11 @@ trait Mountable { next_sibling: NodeRef, ) -> Box; fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); + fn render_to_html<'a>( + &'a self, + w: &'a HtmlWriter, + parent_scope: &'a AnyScope, + ) -> LocalBoxFuture<'a, ()>; } struct PropsWrapper { @@ -218,6 +225,18 @@ impl Mountable for PropsWrapper { let scope: Scope = scope.to_any().downcast(); scope.reuse(self.props, node_ref, next_sibling); } + + fn render_to_html<'a>( + &'a self, + w: &'a HtmlWriter, + parent_scope: &'a AnyScope, + ) -> LocalBoxFuture<'a, ()> { + async move { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + scope.render_to_html(w, self.props.clone()).await; + } + .boxed_local() + } } impl VDiff for VComp { @@ -237,7 +256,10 @@ impl VDiff for VComp { next_sibling: NodeRef, ancestor: Option, ) -> NodeRef { - let mountable = self.props.take().expect("VComp has already been mounted"); + let mountable = self + .mountable + .take() + .expect("VComp has already been mounted"); if let Some(mut ancestor) = ancestor { if let VNode::VComp(ref mut vcomp) = &mut ancestor { @@ -283,6 +305,22 @@ impl fmt::Debug for VChild { } } +mod feat_ssr { + use super::*; + use crate::html_writer::HtmlWriter; + + impl VComp { + pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { + self.mountable + .as_ref() + .map(|m| m.copy()) + .unwrap() + .render_to_html(w, parent_scope) + .await; + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 420a12f900f..04c814fab5a 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -284,6 +284,19 @@ impl VList { } } +mod feat_ssr { + use super::*; + use crate::html_writer::HtmlWriter; + + impl VList { + pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { + for node in self.children.iter() { + node.render_to_html(w, parent_scope).await; + } + } + } +} + impl VDiff for VList { fn detach(&mut self, parent: &Element) { for mut child in self.children.drain(..) { diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index e2af46a89cf..f6aaeb32c01 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -296,6 +296,42 @@ impl PartialEq for VNode { } } +mod feat_ssr { + use futures::future::{FutureExt, LocalBoxFuture}; + + use super::*; + use crate::html_writer::HtmlWriter; + + impl VNode { + pub(crate) fn render_to_html<'a>( + &'a self, + w: &'a HtmlWriter, + parent_scope: &'a AnyScope, // we box here due to: https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html + ) -> LocalBoxFuture<'a, ()> { + async move { + match self { + VNode::VTag(vtag) => vtag.render_to_html(w, parent_scope).await, + VNode::VText(vtext) => vtext.render_to_html(w).await, + VNode::VComp(vcomp) => vcomp.render_to_html(w, parent_scope).await, + VNode::VList(vlist) => vlist.render_to_html(w, parent_scope).await, + // We are pretty safe here as it's not possible to get a web_sys::Node without DOM + // support in the first place. + // + // The only exception would be to use `YewServerRenderer` in a browser or wasm32 with + // jsdom present environment, in which case, it's uncharted territory. + VNode::VRef(_) => { + panic!("VRef is not possible to be rendered in to a html writer.") + } + // Portals are not rendered. + VNode::VPortal(_) => {} + VNode::VSuspense(vsuspense) => vsuspense.render_to_html(w, parent_scope).await, + } + } + .boxed_local() + } + } +} + #[cfg(test)] mod layout_tests { use super::*; diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 5cf6d177510..973598d9b68 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -145,3 +145,15 @@ impl VDiff for VSuspense { } } } + +mod feat_ssr { + use super::*; + use crate::html_writer::HtmlWriter; + + impl VSuspense { + pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { + // always render children on the server side. + self.children.render_to_html(w, parent_scope).await; + } + } +} diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index c69260ab8f7..50fc165987c 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -637,6 +637,44 @@ impl PartialEq for VTag { } } +mod feat_ssr { + use super::*; + use crate::html_writer::HtmlWriter; + + impl Value { + fn as_str(&self) -> Option<&str> { + self.0.as_ref().map(|m| m.as_ref()) + } + } + + impl VTagInner { + fn value(&self) -> Option<&str> { + match self { + Self::Input(ref m) => m.value.as_str(), + Self::Textarea { ref value } => value.as_str(), + _ => None, + } + } + + fn checked(&self) -> bool { + match self { + Self::Input(ref m) => m.checked, + _ => false, + } + } + } + + impl VTag { + pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { + w.push_str("<"); + w.push_str(self.tag()); + // w.push_str() + + w.push_str(">"); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 9b67aa665bb..eb7f4b7c8a0 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -28,6 +28,18 @@ impl VText { } } +mod feat_ssr { + use super::*; + + use crate::html_writer::HtmlWriter; + + impl VText { + pub(crate) async fn render_to_html(&self, w: &HtmlWriter) { + w.push_str(&self.text); + } + } +} + impl std::fmt::Debug for VText { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( From 667796d8c01f6d6f4ddf3b3abbd59bf7e15ecb1f Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 6 Jan 2022 19:53:35 +0900 Subject: [PATCH 02/27] Remove HtmlWriter. --- packages/yew/src/html/component/scope.rs | 4 +- packages/yew/src/html_writer.rs | 22 ----------- packages/yew/src/lib.rs | 8 ++-- packages/yew/src/virtual_dom/vcomp.rs | 8 ++-- packages/yew/src/virtual_dom/vlist.rs | 3 +- packages/yew/src/virtual_dom/vnode.rs | 3 +- packages/yew/src/virtual_dom/vsuspense.rs | 3 +- packages/yew/src/virtual_dom/vtag.rs | 47 ++++++++++++++++++++--- packages/yew/src/virtual_dom/vtext.rs | 4 +- 9 files changed, 52 insertions(+), 50 deletions(-) delete mode 100644 packages/yew/src/html_writer.rs diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 90d73263363..e3290076c75 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -418,11 +418,9 @@ impl Scope { mod feat_ssr { use super::*; - use crate::html_writer::HtmlWriter; impl Scope { - /// Renders Into a [`HtmlWrite`]. - pub(crate) async fn render_to_html(&self, w: &HtmlWriter, props: Rc) { + pub(crate) async fn render_to_html(&self, w: &mut String, props: Rc) { let (tx, rx) = oneshot::channel(); scheduler::push_component_create( diff --git a/packages/yew/src/html_writer.rs b/packages/yew/src/html_writer.rs deleted file mode 100644 index 4122d48adf3..00000000000 --- a/packages/yew/src/html_writer.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! module to provide an html writer that can be written without mutable borrowing. - -mod feat_ssr { - use std::cell::RefCell; - - #[derive(Debug, Default)] - pub(crate) struct HtmlWriter { - inner: RefCell, - } - - impl HtmlWriter { - pub fn push_str(&self, s: &str) { - self.inner.borrow_mut().push_str(s); - } - - pub fn into_inner(self) -> String { - self.inner.into_inner() - } - } -} - -pub use feat_ssr::*; diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 1a33af8559d..cf0e15104ae 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -260,7 +260,6 @@ pub mod callback; pub mod context; pub mod functional; pub mod html; -mod html_writer; pub mod scheduler; mod sealed; pub mod suspense; @@ -369,7 +368,6 @@ mod feat_ssr { use super::*; use crate::html::Scope; - use crate::html_writer::HtmlWriter; /// A Yew Server-side Renderer. #[derive(Debug)] @@ -412,12 +410,12 @@ mod feat_ssr { /// Renders Yew Application into a string. pub async fn render_to_string(self) -> String { - let s = HtmlWriter::default(); + let mut s = String::new(); let scope = Scope::::new(None); - scope.render_to_html(&s, self.props.into()).await; + scope.render_to_html(&mut s, self.props.into()).await; - s.into_inner() + s } } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 71fb0e2b1df..b0072b33488 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -2,7 +2,6 @@ use super::{Key, VDiff, VNode}; use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped}; -use crate::html_writer::HtmlWriter; use futures::future::{FutureExt, LocalBoxFuture}; use std::any::TypeId; use std::borrow::Borrow; @@ -185,7 +184,7 @@ trait Mountable { fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); fn render_to_html<'a>( &'a self, - w: &'a HtmlWriter, + w: &'a mut String, parent_scope: &'a AnyScope, ) -> LocalBoxFuture<'a, ()>; } @@ -228,7 +227,7 @@ impl Mountable for PropsWrapper { fn render_to_html<'a>( &'a self, - w: &'a HtmlWriter, + w: &'a mut String, parent_scope: &'a AnyScope, ) -> LocalBoxFuture<'a, ()> { async move { @@ -307,10 +306,9 @@ impl fmt::Debug for VChild { mod feat_ssr { use super::*; - use crate::html_writer::HtmlWriter; impl VComp { - pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { + pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { self.mountable .as_ref() .map(|m| m.copy()) diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 04c814fab5a..46141525409 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -286,10 +286,9 @@ impl VList { mod feat_ssr { use super::*; - use crate::html_writer::HtmlWriter; impl VList { - pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { + pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { for node in self.children.iter() { node.render_to_html(w, parent_scope).await; } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index f6aaeb32c01..a04cbcec8a5 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -300,12 +300,11 @@ mod feat_ssr { use futures::future::{FutureExt, LocalBoxFuture}; use super::*; - use crate::html_writer::HtmlWriter; impl VNode { pub(crate) fn render_to_html<'a>( &'a self, - w: &'a HtmlWriter, + w: &'a mut String, parent_scope: &'a AnyScope, // we box here due to: https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html ) -> LocalBoxFuture<'a, ()> { async move { diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 973598d9b68..86e5210183f 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -148,10 +148,9 @@ impl VDiff for VSuspense { mod feat_ssr { use super::*; - use crate::html_writer::HtmlWriter; impl VSuspense { - pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { + pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { // always render children on the server side. self.children.render_to_html(w, parent_scope).await; } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 50fc165987c..2b0ad455329 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -639,7 +639,7 @@ impl PartialEq for VTag { mod feat_ssr { use super::*; - use crate::html_writer::HtmlWriter; + use std::fmt::Write; impl Value { fn as_str(&self) -> Option<&str> { @@ -665,12 +665,47 @@ mod feat_ssr { } impl VTag { - pub(crate) async fn render_to_html(&self, w: &HtmlWriter, parent_scope: &AnyScope) { - w.push_str("<"); - w.push_str(self.tag()); - // w.push_str() + pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { + write!(w, "<{}", self.tag()).unwrap(); - w.push_str(">"); + let write_attr = |w: &mut String, name: &str, val: Option<&str>| { + write!(w, " {}", name).unwrap(); + + if let Some(m) = val { + // TODO: Escape. + write!(w, "=\"{}\"", m).unwrap(); + } + }; + + if let Some(m) = self.inner.value() { + write_attr(w, "value", Some(m)); + } + + if self.inner.checked() { + write_attr(w, "checked", None); + } + + for (k, v) in self.attributes.iter() { + write_attr(w, k, Some(v)); + } + + write!(w, ">").unwrap(); + + match self.inner { + VTagInner::Input(_) => {} + VTagInner::Textarea { .. } => write!(w, "").unwrap(), + VTagInner::Other { + ref tag, + ref children, + .. + } => { + for child in children.iter() { + child.render_to_html(w, parent_scope).await; + + write!(w, "", tag).unwrap(); + } + } + } } } } diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index eb7f4b7c8a0..29c2aa933cb 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -31,10 +31,8 @@ impl VText { mod feat_ssr { use super::*; - use crate::html_writer::HtmlWriter; - impl VText { - pub(crate) async fn render_to_html(&self, w: &HtmlWriter) { + pub(crate) async fn render_to_html(&self, w: &mut String) { w.push_str(&self.text); } } From 820d3ac071a6205997f36df3edb92b68d26bfc38 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 6 Jan 2022 20:15:15 +0900 Subject: [PATCH 03/27] Escape html content. --- packages/yew/Cargo.toml | 3 ++- packages/yew/src/virtual_dom/vtag.rs | 36 +++++---------------------- packages/yew/src/virtual_dom/vtext.rs | 2 +- 3 files changed, 9 insertions(+), 32 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index d14d596739b..adfdbd151c3 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -26,11 +26,12 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" yew-macro = { version = "^0.19.0", path = "../yew-macro" } thiserror = "1.0" -async-trait = "0.1" futures = "0.3" scoped-tls-hkt = "0.1" +html-escape = "0.2.9" + [dependencies.web-sys] version = "0.3" features = [ diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 2b0ad455329..186fa8ed5ae 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -384,8 +384,8 @@ impl VTag { /// Returns `checked` property of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). /// (Not a value of node's attribute). - pub fn checked(&mut self) -> bool { - match &mut self.inner { + pub fn checked(&self) -> bool { + match &self.inner { VTagInner::Input(f) => f.checked, _ => false, } @@ -641,29 +641,6 @@ mod feat_ssr { use super::*; use std::fmt::Write; - impl Value { - fn as_str(&self) -> Option<&str> { - self.0.as_ref().map(|m| m.as_ref()) - } - } - - impl VTagInner { - fn value(&self) -> Option<&str> { - match self { - Self::Input(ref m) => m.value.as_str(), - Self::Textarea { ref value } => value.as_str(), - _ => None, - } - } - - fn checked(&self) -> bool { - match self { - Self::Input(ref m) => m.checked, - _ => false, - } - } - } - impl VTag { pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { write!(w, "<{}", self.tag()).unwrap(); @@ -672,16 +649,15 @@ mod feat_ssr { write!(w, " {}", name).unwrap(); if let Some(m) = val { - // TODO: Escape. - write!(w, "=\"{}\"", m).unwrap(); + write!(w, "=\"{}\"", html_escape::encode_double_quoted_attribute(m)).unwrap(); } }; - if let Some(m) = self.inner.value() { + if let Some(m) = self.value() { write_attr(w, "value", Some(m)); } - if self.inner.checked() { + if self.checked() { write_attr(w, "checked", None); } @@ -693,7 +669,7 @@ mod feat_ssr { match self.inner { VTagInner::Input(_) => {} - VTagInner::Textarea { .. } => write!(w, "").unwrap(), + VTagInner::Textarea { .. } => w.push_str(""), VTagInner::Other { ref tag, ref children, diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 29c2aa933cb..e98e6ffb291 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -33,7 +33,7 @@ mod feat_ssr { impl VText { pub(crate) async fn render_to_html(&self, w: &mut String) { - w.push_str(&self.text); + html_escape::encode_text_to_string(&self.text, w); } } } From 1b2d823d3a5caf63cf101f7d89be5559501ef604 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 6 Jan 2022 21:40:24 +0900 Subject: [PATCH 04/27] Add non-suspense tests. --- Makefile.toml | 7 +- packages/yew/Cargo.toml | 3 + packages/yew/Makefile.toml | 4 + packages/yew/src/virtual_dom/vcomp.rs | 41 ++++++++++ packages/yew/src/virtual_dom/vlist.rs | 24 ++++++ packages/yew/src/virtual_dom/vtag.rs | 103 ++++++++++++++++++++++++-- packages/yew/src/virtual_dom/vtext.rs | 18 +++++ 7 files changed, 191 insertions(+), 9 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index 7f4a7ea2670..c0b7cb7fd75 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -38,7 +38,7 @@ category = "Testing" description = "Run all tests" dependencies = ["tests-setup"] env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*", "**/packages/changelog"] } -run_task = { name = ["test-flow", "doc-test-flow", "website-test"], fork = true } +run_task = { name = ["test-flow", "doc-test-flow", "ssr-test", "website-test"], fork = true } [tasks.benchmarks] category = "Testing" @@ -117,3 +117,8 @@ category = "Maintainer processes" toolchain = "stable" command = "cargo" args = ["run","-p","changelog", "--release", "${@}"] + +[tasks.ssr-test] +env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/yew"] } +private = true +workspace = true diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index adfdbd151c3..aba428cdb49 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -73,5 +73,8 @@ doc_test = [] wasm_test = [] wasm_bench = [] +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { version = "1.15.0", features = ["full"] } + [package.metadata.docs.rs] features = ["doc_test"] diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index d216f7b30b8..c438b178f92 100644 --- a/packages/yew/Makefile.toml +++ b/packages/yew/Makefile.toml @@ -35,3 +35,7 @@ args = [ "wasm_bench", "bench", ] + +[tasks.ssr-test] +command = "cargo" +args = ["test", "ssr_tests"] diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index b0072b33488..48457b6adf6 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -903,3 +903,44 @@ mod layout_tests { diff_layouts(vec![layout]); } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod ssr_tests { + use tokio::test; + + use crate::prelude::*; + use crate::YewServerRenderer; + + #[test] + async fn test_props() { + #[derive(PartialEq, Properties, Debug)] + struct ChildProps { + name: String, + } + + #[function_component] + fn Child(props: &ChildProps) -> Html { + html! {
{"Hello, "}{&props.name}{"!"}
} + } + + #[function_component] + fn Comp() -> Html { + html! { +
+ + + +
+ } + } + + let renderer = YewServerRenderer::::new(); + + let s = renderer.render_to_string().await; + + assert_eq!( + s, + "
Hello, Jane!
Hello, John!
Hello, Josh!
" + ); + } +} diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 46141525409..1efaf146b8c 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1279,3 +1279,27 @@ mod layout_tests_keys { diff_layouts(layouts); } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod ssr_tests { + use tokio::test; + + use crate::prelude::*; + use crate::YewServerRenderer; + + #[test] + async fn test_text_back_to_back() { + #[function_component] + fn Comp() -> Html { + let s = "world"; + + html! {
{"Hello "}{s}{"!"}
} + } + + let renderer = YewServerRenderer::::new(); + + let s = renderer.render_to_string().await; + + assert_eq!(s, "
Hello world!
"); + } +} diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 186fa8ed5ae..d36dc3de3c3 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -639,6 +639,7 @@ impl PartialEq for VTag { mod feat_ssr { use super::*; + use crate::virtual_dom::VText; use std::fmt::Write; impl VTag { @@ -653,12 +654,14 @@ mod feat_ssr { } }; - if let Some(m) = self.value() { - write_attr(w, "value", Some(m)); - } + if let VTagInner::Input(_) = self.inner { + if let Some(m) = self.value() { + write_attr(w, "value", Some(m)); + } - if self.checked() { - write_attr(w, "checked", None); + if self.checked() { + write_attr(w, "checked", None); + } } for (k, v) in self.attributes.iter() { @@ -669,7 +672,13 @@ mod feat_ssr { match self.inner { VTagInner::Input(_) => {} - VTagInner::Textarea { .. } => w.push_str(""), + VTagInner::Textarea { .. } => { + if let Some(m) = self.value() { + VText::new(m.to_owned()).render_to_html(w).await; + } + + w.push_str(""); + } VTagInner::Other { ref tag, ref children, @@ -677,9 +686,9 @@ mod feat_ssr { } => { for child in children.iter() { child.render_to_html(w, parent_scope).await; - - write!(w, "", tag).unwrap(); } + + write!(w, "", tag).unwrap(); } } } @@ -1488,3 +1497,81 @@ mod tests_without_browser { ); } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod ssr_tests { + use tokio::test; + + use crate::prelude::*; + use crate::YewServerRenderer; + + #[test] + async fn test_simple_tag() { + #[function_component] + fn Comp() -> Html { + html! {
} + } + + let renderer = YewServerRenderer::::new(); + + let s = renderer.render_to_string().await; + + assert_eq!(s, "
"); + } + + #[test] + async fn test_simple_tag_with_attr() { + #[function_component] + fn Comp() -> Html { + html! {
} + } + + let renderer = YewServerRenderer::::new(); + + let s = renderer.render_to_string().await; + + assert_eq!(s, r#"
"#); + } + + #[test] + async fn test_simple_tag_with_content() { + #[function_component] + fn Comp() -> Html { + html! {
{"Hello!"}
} + } + + let renderer = YewServerRenderer::::new(); + + let s = renderer.render_to_string().await; + + assert_eq!(s, r#"
Hello!
"#); + } + + #[test] + async fn test_simple_tag_with_nested_tag_and_input() { + #[function_component] + fn Comp() -> Html { + html! {
{"Hello!"}
} + } + + let renderer = YewServerRenderer::::new(); + + let s = renderer.render_to_string().await; + + assert_eq!(s, r#"
Hello!
"#); + } + + #[test] + async fn test_textarea() { + #[function_component] + fn Comp() -> Html { + html! { "#); + } +} diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index e98e6ffb291..7be42d092df 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -190,3 +190,21 @@ mod layout_tests { diff_layouts(vec![layout1, layout2, layout3, layout4]); } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod ssr_tests { + use tokio::test; + + use super::*; + + #[test] + async fn test_simple_str() { + let vtext = VText::new("abc"); + + let mut s = String::new(); + + vtext.render_to_html(&mut s).await; + + assert_eq!("abc", s.as_str()); + } +} From 5b3f5e356e104289b422190a21e108bfcededc28 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 6 Jan 2022 22:18:02 +0900 Subject: [PATCH 05/27] Add Suspense tests. --- packages/yew/src/html/component/lifecycle.rs | 4 +- packages/yew/src/suspense/component.rs | 12 +- packages/yew/src/virtual_dom/vlist.rs | 33 ++++++ packages/yew/src/virtual_dom/vsuspense.rs | 113 +++++++++++++++++-- 4 files changed, 151 insertions(+), 11 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 60d4d1cb3a2..527fb329a73 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -258,7 +258,9 @@ impl Runnable for RenderRunner { let comp_scope = AnyScope::from(state.context.scope.clone()); - let suspense_scope = comp_scope.find_parent_scope::().unwrap(); + let suspense_scope = comp_scope + .find_parent_scope::() + .expect("To suspend rendering, a component is required."); let suspense = suspense_scope.get_component().unwrap(); m.listen(Callback::from(move |_| { diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs index 750f2d9f78b..66e35422a9a 100644 --- a/packages/yew/src/suspense/component.rs +++ b/packages/yew/src/suspense/component.rs @@ -1,7 +1,6 @@ use crate::html::{Children, Component, Context, Html, Properties, Scope}; use crate::virtual_dom::{Key, VList, VNode, VSuspense}; -use gloo_utils::document; use web_sys::Element; use super::Suspension; @@ -29,7 +28,7 @@ pub enum SuspenseMsg { pub struct Suspense { link: Scope, suspensions: Vec, - detached_parent: Element, + detached_parent: Option, } impl Component for Suspense { @@ -40,7 +39,14 @@ impl Component for Suspense { Self { link: ctx.link().clone(), suspensions: Vec::new(), - detached_parent: document().create_element("div").unwrap(), + + #[cfg(target_arch = "wasm32")] + detached_parent: web_sys::window() + .and_then(|m| m.document()) + .and_then(|m| m.create_element("div").ok()), + + #[cfg(not(target_arch = "wasm32"))] + detached_parent: None, } } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 1efaf146b8c..e065822df23 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1302,4 +1302,37 @@ mod ssr_tests { assert_eq!(s, "
Hello world!
"); } + + #[test] + async fn test_fragment() { + #[derive(PartialEq, Properties, Debug)] + struct ChildProps { + name: String, + } + + #[function_component] + fn Child(props: &ChildProps) -> Html { + html! {
{"Hello, "}{&props.name}{"!"}
} + } + + #[function_component] + fn Comp() -> Html { + html! { + <> + + + + + } + } + + let renderer = YewServerRenderer::::new(); + + let s = renderer.render_to_string().await; + + assert_eq!( + s, + "
Hello, Jane!
Hello, John!
Hello, Josh!
" + ); + } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 86e5210183f..157780a8e38 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -12,7 +12,7 @@ pub struct VSuspense { fallback: Box, /// The element to attach to when children is not attached to DOM - detached_parent: Element, + detached_parent: Option, /// Whether the current status is suspended. suspended: bool, @@ -25,7 +25,7 @@ impl VSuspense { pub(crate) fn new( children: VNode, fallback: VNode, - detached_parent: Element, + detached_parent: Option, suspended: bool, key: Option, ) -> Self { @@ -51,7 +51,9 @@ impl VDiff for VSuspense { fn detach(&mut self, parent: &Element) { if self.suspended { self.fallback.detach(parent); - self.children.detach(&self.detached_parent); + if let Some(ref m) = self.detached_parent { + self.children.detach(m); + } } else { self.children.detach(parent); } @@ -74,6 +76,8 @@ impl VDiff for VSuspense { next_sibling: NodeRef, ancestor: Option, ) -> NodeRef { + let detached_parent = self.detached_parent.as_ref().expect("no detached parent?"); + let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor { Some(VNode::VSuspense(mut m)) => { // We only preserve the child state if they are the same suspense. @@ -98,7 +102,7 @@ impl VDiff for VSuspense { (true, true) => { self.children.apply( parent_scope, - &self.detached_parent, + detached_parent, NodeRef::default(), children_ancestor, ); @@ -115,13 +119,13 @@ impl VDiff for VSuspense { (true, false) => { children_ancestor.as_ref().unwrap().shift( parent, - &self.detached_parent, + detached_parent, NodeRef::default(), ); self.children.apply( parent_scope, - &self.detached_parent, + detached_parent, NodeRef::default(), children_ancestor, ); @@ -135,7 +139,7 @@ impl VDiff for VSuspense { fallback_ancestor.unwrap().detach(parent); children_ancestor.as_ref().unwrap().shift( - &self.detached_parent, + detached_parent, parent, next_sibling.clone(), ); @@ -156,3 +160,98 @@ mod feat_ssr { } } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod ssr_tests { + use std::rc::Rc; + use std::time::Duration; + + use tokio::task::{spawn_local, LocalSet}; + use tokio::test; + use tokio::time::sleep; + + use crate::prelude::*; + use crate::suspense::{Suspension, SuspensionResult}; + use crate::YewServerRenderer; + + #[test(flavor = "multi_thread", worker_threads = 2)] + async fn test_suspense() { + #[derive(PartialEq)] + pub struct SleepState { + s: Suspension, + } + + impl SleepState { + fn new() -> Self { + let (s, handle) = Suspension::new(); + + // we use tokio spawn local here. + spawn_local(async move { + // we use tokio sleep here. + sleep(Duration::from_millis(50)).await; + + handle.resume(); + }); + + Self { s } + } + } + + impl Reducible for SleepState { + type Action = (); + + fn reduce(self: Rc, _action: Self::Action) -> Rc { + Self::new().into() + } + } + + pub fn use_sleep() -> SuspensionResult> { + let sleep_state = use_reducer(SleepState::new); + + if sleep_state.s.resumed() { + Ok(Rc::new(move || sleep_state.dispatch(()))) + } else { + Err(sleep_state.s.clone()) + } + } + + #[derive(PartialEq, Properties, Debug)] + struct ChildProps { + name: String, + } + + #[function_component] + fn Child(props: &ChildProps) -> HtmlResult { + use_sleep()?; + Ok(html! {
{"Hello, "}{&props.name}{"!"}
}) + } + + #[function_component] + fn Comp() -> Html { + let fallback = html! {"loading..."}; + + html! { + + + + + + } + } + + let local = LocalSet::new(); + + let s = local + .run_until(async move { + let renderer = YewServerRenderer::::new(); + + renderer.render_to_string().await + }) + .await; + + assert_eq!( + s, + "
Hello, Jane!
Hello, John!
Hello, Josh!
" + ); + } +} From c7c9d2d318b285360704e78d3aa15215214388d7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 6 Jan 2022 22:29:05 +0900 Subject: [PATCH 06/27] Gated "ssr" feature. --- packages/yew/Cargo.toml | 5 +++-- packages/yew/Makefile.toml | 2 +- packages/yew/src/html/component/lifecycle.rs | 14 +++++++++++--- packages/yew/src/html/component/scope.rs | 4 +++- packages/yew/src/lib.rs | 2 ++ packages/yew/src/virtual_dom/vcomp.rs | 7 ++++++- packages/yew/src/virtual_dom/vlist.rs | 3 ++- packages/yew/src/virtual_dom/vnode.rs | 1 + packages/yew/src/virtual_dom/vsuspense.rs | 3 ++- packages/yew/src/virtual_dom/vtag.rs | 3 ++- packages/yew/src/virtual_dom/vtext.rs | 3 ++- 11 files changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index aba428cdb49..2e1f2c0ccca 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -26,11 +26,11 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" yew-macro = { version = "^0.19.0", path = "../yew-macro" } thiserror = "1.0" -futures = "0.3" scoped-tls-hkt = "0.1" -html-escape = "0.2.9" +futures = { version = "0.3", optional = true } +html-escape = { version = "0.2.9", optional = true } [dependencies.web-sys] version = "0.3" @@ -72,6 +72,7 @@ gloo = { version = "0.4", features = ["futures"] } doc_test = [] wasm_test = [] wasm_bench = [] +ssr = ["futures", "html-escape"] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.15.0", features = ["full"] } diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index c438b178f92..4753090ddb3 100644 --- a/packages/yew/Makefile.toml +++ b/packages/yew/Makefile.toml @@ -38,4 +38,4 @@ args = [ [tasks.ssr-test] command = "cargo" -args = ["test", "ssr_tests"] +args = ["test", "ssr_tests", "--features", "ssr"] diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 527fb329a73..1ec5986b61c 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -7,6 +7,7 @@ use crate::suspense::{Suspense, Suspension}; use crate::virtual_dom::{VDiff, VNode}; use crate::Callback; use crate::{Context, NodeRef}; +#[cfg(feature = "ssr")] use futures::channel::oneshot; use std::rc::Rc; use web_sys::Element; @@ -26,6 +27,7 @@ pub(crate) struct ComponentState { suspension: Option, + #[cfg(feature = "ssr")] html_sender: Option>, // Used for debug logging @@ -41,7 +43,7 @@ impl ComponentState { node_ref: NodeRef, scope: Scope, props: Rc, - html_sender: Option>, + #[cfg(feature = "ssr")] html_sender: Option>, ) -> Self { #[cfg(debug_assertions)] let vcomp_id = { @@ -62,6 +64,7 @@ impl ComponentState { suspension: None, has_rendered: false, + #[cfg(feature = "ssr")] html_sender, #[cfg(debug_assertions)] @@ -77,6 +80,7 @@ pub(crate) struct CreateRunner { pub(crate) node_ref: NodeRef, pub(crate) props: Rc, pub(crate) scope: Scope, + #[cfg(feature = "ssr")] pub(crate) html_sender: Option>, } @@ -94,6 +98,7 @@ impl Runnable for CreateRunner { self.node_ref, self.scope.clone(), self.props, + #[cfg(feature = "ssr")] self.html_sender, )); } @@ -231,8 +236,11 @@ impl Runnable for RenderRunner { let node = new_root.apply(&scope, m, next_sibling, ancestor); state.node_ref.link(node); - } else if let Some(tx) = state.html_sender.take() { - tx.send(root).unwrap(); + } else { + #[cfg(feature = "ssr")] + if let Some(tx) = state.html_sender.take() { + tx.send(root).unwrap(); + } } } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index e3290076c75..4b950d9edaa 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -12,7 +12,6 @@ use crate::context::{ContextHandle, ContextProvider}; use crate::html::NodeRef; use crate::scheduler::{self, Shared}; use crate::virtual_dom::{insert_node, VNode}; -use futures::channel::oneshot; use gloo_utils::document; use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell}; @@ -241,6 +240,7 @@ impl Scope { node_ref, props, scope: self.clone(), + #[cfg(feature = "ssr")] html_sender: None, }, RenderRunner { @@ -416,8 +416,10 @@ impl Scope { } } +#[cfg(feature = "ssr")] mod feat_ssr { use super::*; + use futures::channel::oneshot; impl Scope { pub(crate) async fn render_to_html(&self, w: &mut String, props: Rc) { diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index cf0e15104ae..9ecb86ff42e 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -364,6 +364,7 @@ fn set_default_panic_hook() { // } // } +#[cfg(feature = "ssr")] mod feat_ssr { use super::*; @@ -420,6 +421,7 @@ mod feat_ssr { } } +#[cfg(feature = "ssr")] pub use feat_ssr::*; /// The main entry point of a Yew application. diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 48457b6adf6..6df381325b9 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -2,6 +2,7 @@ use super::{Key, VDiff, VNode}; use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped}; +#[cfg(feature = "ssr")] use futures::future::{FutureExt, LocalBoxFuture}; use std::any::TypeId; use std::borrow::Borrow; @@ -182,6 +183,8 @@ trait Mountable { next_sibling: NodeRef, ) -> Box; fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); + + #[cfg(feature = "ssr")] fn render_to_html<'a>( &'a self, w: &'a mut String, @@ -225,6 +228,7 @@ impl Mountable for PropsWrapper { scope.reuse(self.props, node_ref, next_sibling); } + #[cfg(feature = "ssr")] fn render_to_html<'a>( &'a self, w: &'a mut String, @@ -304,6 +308,7 @@ impl fmt::Debug for VChild { } } +#[cfg(feature = "ssr")] mod feat_ssr { use super::*; @@ -904,7 +909,7 @@ mod layout_tests { } } -#[cfg(all(test, not(target_arch = "wasm32")))] +#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use tokio::test; diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index e065822df23..f440305b10d 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -284,6 +284,7 @@ impl VList { } } +#[cfg(feature = "ssr")] mod feat_ssr { use super::*; @@ -1280,7 +1281,7 @@ mod layout_tests_keys { } } -#[cfg(all(test, not(target_arch = "wasm32")))] +#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use tokio::test; diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index a04cbcec8a5..a0e2f9ffc2a 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -296,6 +296,7 @@ impl PartialEq for VNode { } } +#[cfg(feature = "ssr")] mod feat_ssr { use futures::future::{FutureExt, LocalBoxFuture}; diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 157780a8e38..11217ff4b63 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -150,6 +150,7 @@ impl VDiff for VSuspense { } } +#[cfg(feature = "ssr")] mod feat_ssr { use super::*; @@ -161,7 +162,7 @@ mod feat_ssr { } } -#[cfg(all(test, not(target_arch = "wasm32")))] +#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use std::rc::Rc; use std::time::Duration; diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index d36dc3de3c3..350a83d390a 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -637,6 +637,7 @@ impl PartialEq for VTag { } } +#[cfg(feature = "ssr")] mod feat_ssr { use super::*; use crate::virtual_dom::VText; @@ -1498,7 +1499,7 @@ mod tests_without_browser { } } -#[cfg(all(test, not(target_arch = "wasm32")))] +#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use tokio::test; diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 7be42d092df..ee5c8233866 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -28,6 +28,7 @@ impl VText { } } +#[cfg(feature = "ssr")] mod feat_ssr { use super::*; @@ -191,7 +192,7 @@ mod layout_tests { } } -#[cfg(all(test, not(target_arch = "wasm32")))] +#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use tokio::test; From 2bab219269bbb8cc415396f333172e1bc020954c Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 01:15:40 +0900 Subject: [PATCH 07/27] Add example. --- .github/workflows/publish-examples.yml | 5 + Cargo.toml | 1 + examples/simple_ssr/Cargo.toml | 13 ++ examples/simple_ssr/src/main.rs | 129 ++++++++++++++++++ packages/yew/src/lib.rs | 13 +- packages/yew/src/virtual_dom/vcomp.rs | 2 +- packages/yew/src/virtual_dom/vlist.rs | 4 +- packages/yew/src/virtual_dom/vsuspense.rs | 2 +- packages/yew/src/virtual_dom/vtag.rs | 8 +- tools/website-test/Cargo.toml | 3 +- tools/website-test/build.rs | 2 +- .../advanced-topics/server-side-rendering.md | 108 +++++++++++++++ website/docs/concepts/suspense.md | 2 +- website/sidebars.js | 1 + 14 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 examples/simple_ssr/Cargo.toml create mode 100644 examples/simple_ssr/src/main.rs create mode 100644 website/docs/advanced-topics/server-side-rendering.md diff --git a/.github/workflows/publish-examples.yml b/.github/workflows/publish-examples.yml index af3c9ee72e1..e62f272f535 100644 --- a/.github/workflows/publish-examples.yml +++ b/.github/workflows/publish-examples.yml @@ -61,6 +61,11 @@ jobs: continue fi + # ssr does not need trunk + if [[ "$example" == "simple_ssr" ]]; then + continue + fi + echo "building: $example" ( cd "$path" diff --git a/Cargo.toml b/Cargo.toml index 862adb4c4ad..a6490ac3703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "examples/password_strength", "examples/portals", "examples/router", + "examples/simple_ssr", "examples/timer", "examples/todomvc", "examples/two_apps", diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml new file mode 100644 index 00000000000..da8904cf2e4 --- /dev/null +++ b/examples/simple_ssr/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "simple_ssr" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.15.0", features = ["full"] } +warp = "0.3" +yew = { path = "../../packages/yew", features = ["ssr"] } +reqwest = { version = "0.11.8", features = ["json"] } +serde = { version = "1.0.132", features = ["derive"] } diff --git a/examples/simple_ssr/src/main.rs b/examples/simple_ssr/src/main.rs new file mode 100644 index 00000000000..69391621b87 --- /dev/null +++ b/examples/simple_ssr/src/main.rs @@ -0,0 +1,129 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use serde::{Deserialize, Serialize}; +use tokio::task::LocalSet; +use tokio::task::{spawn_blocking, spawn_local}; +use warp::Filter; +use yew::prelude::*; +use yew::suspense::{Suspension, SuspensionResult}; + +#[derive(Serialize, Deserialize)] +struct UuidResponse { + uuid: String, +} + +async fn fetch_uuid() -> Rc { + // reqwest works for both non-wasm and wasm targets. + let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap(); + let uuid_resp = resp.json::().await.unwrap(); + + uuid_resp.uuid.into() +} + +pub struct UuidState { + s: Suspension, + value: Rc>>>, +} + +impl UuidState { + fn new() -> Self { + let (s, handle) = Suspension::new(); + let value: Rc>>> = Rc::default(); + + { + let value = value.clone(); + // we use tokio spawn local here. + spawn_local(async move { + let uuid = fetch_uuid().await; + + { + let mut value = value.borrow_mut(); + *value = Some(uuid); + } + + handle.resume(); + }); + } + + Self { s, value } + } +} + +impl PartialEq for UuidState { + fn eq(&self, rhs: &Self) -> bool { + self.s == rhs.s + } +} + +fn use_random_uuid() -> SuspensionResult> { + let s = use_state(UuidState::new); + + let result = match *s.value.borrow() { + Some(ref m) => Ok(m.clone()), + None => Err(s.s.clone()), + }; + + result +} + +#[function_component] +fn Content() -> HtmlResult { + let uuid = use_random_uuid()?; + + Ok(html! { +
{"Random UUID: "}{uuid}
+ }) +} + +#[function_component] +fn App() -> Html { + let fallback = html! {
{"Loading..."}
}; + + html! { + + + + } +} + +async fn render() -> String { + let content = spawn_blocking(move || { + use tokio::runtime::Builder; + let set = LocalSet::new(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + set.block_on(&rt, async { + let renderer = yew::YewServerRenderer::::new(); + + renderer.render().await + }) + }) + .await + .expect("the thread has failed."); + + format!( + r#" + + + Yew SSR Example + + + {} + + +"#, + content + ) +} + +#[tokio::main] +async fn main() { + // Match any request and return hello world! + let routes = warp::any().then(|| async move { warp::reply::html(render().await) }); + + println!("You can view the website at: http://localhost:8080/"); + + warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +} diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 9ecb86ff42e..3bac1cce100 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -409,15 +409,20 @@ mod feat_ssr { Self { props } } - /// Renders Yew Application into a string. - pub async fn render_to_string(self) -> String { + /// Renders Yew Application. + pub async fn render(self) -> String { let mut s = String::new(); - let scope = Scope::::new(None); - scope.render_to_html(&mut s, self.props.into()).await; + self.render_to_string(&mut s).await; s } + + /// Renders Yew Application to a String. + pub async fn render_to_string(self, w: &mut String) { + let scope = Scope::::new(None); + scope.render_to_html(w, self.props.into()).await; + } } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 6df381325b9..a338134b195 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -941,7 +941,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!( s, diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index f440305b10d..e366d297fbb 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1299,7 +1299,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!(s, "
Hello world!
"); } @@ -1329,7 +1329,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!( s, diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 11217ff4b63..0b679c2adca 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -246,7 +246,7 @@ mod ssr_tests { .run_until(async move { let renderer = YewServerRenderer::::new(); - renderer.render_to_string().await + renderer.render().await }) .await; diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 350a83d390a..6c48b7b2984 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1515,7 +1515,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!(s, "
"); } @@ -1529,7 +1529,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!(s, r#"
"#); } @@ -1557,7 +1557,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!(s, r#"
Hello!
"#); } @@ -1571,7 +1571,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!(s, r#""#); } diff --git a/tools/website-test/Cargo.toml b/tools/website-test/Cargo.toml index 1b304b06faf..c2d534ed35b 100644 --- a/tools/website-test/Cargo.toml +++ b/tools/website-test/Cargo.toml @@ -16,8 +16,9 @@ js-sys = "0.3" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" weblog = "0.3.0" -yew = { path = "../../packages/yew/" } +yew = { path = "../../packages/yew/", features = ["ssr"] } yew-router = { path = "../../packages/yew-router/" } +tokio = { version = "1.15.0", features = ["full"] } [dev-dependencies.web-sys] version = "0.3" diff --git a/tools/website-test/build.rs b/tools/website-test/build.rs index d4d86e17826..05fc1298384 100644 --- a/tools/website-test/build.rs +++ b/tools/website-test/build.rs @@ -13,7 +13,7 @@ struct Level { fn main() { let home = env::var("CARGO_MANIFEST_DIR").unwrap(); - let pattern = format!("{}/../../website/docs/**/*.mdx", home); + let pattern = format!("{}/../../website/docs/**/*.md*", home); let base = format!("{}/../../website", home); let base = Path::new(&base).canonicalize().unwrap(); let dir_pattern = format!("{}/../../website/docs/**", home); diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md new file mode 100644 index 00000000000..9236fe316ec --- /dev/null +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -0,0 +1,108 @@ +--- +title: "Server-side Rendering" +description: "Render Yew on the server-side." +--- + +# Server-side Rendering + +By default, Yew applications renders at the client side. That is, a skeleton +html file without any actual content and a WebAssembly bundle is +downloaded to the browser and everything is rendered at the client side. +This is known as client-side rendering. + +This approach works fine for most websites as most users now use a modern +browser and devices with adequate computing power. + +However, there're some caveats with client-side rendering: + +1. The user will not be able to see anything until the entire application is + downloaded and initial render has completed. +2. Some search engines do not support dynamically rendered web content and + those who do usually rank dynamic websites lower in the search results. + +To solve these problems, we can render our website on the server side. + +## How it Works + +Yew provides a `YewServerRenderer` renderer to render pages on the +server-side. + +You can create a render with `YewServerRenderer::::new()`. +And calling `renderer.render().await` will render `` +into a `String`. + +```rust +use yew::prelude::*; +use yew::YewServerRenderer; + +#[function_component] +fn App() -> Html { + html! {
{"Hello, World!"}
} +} + +#[tokio::main] +async fn main() { + let renderer = YewServerRenderer::::new(); + + let rendered = renderer.render().await; + + // Prints:
Hello, World!
+ println!("{}", rendered); +} +``` + +## Component Lifecycle + +The recommended way of working with server-side rendering is +function components. + +All hooks other than `use_effect` will function normally until a component +successfully renders into `Html` for the first time. + +Web APIs such as `web_sys` are not available when using server-side rendering. +You application will panic if you try to use them. +You should isolate logics that need Web APIs in `use_effect` or +`use_effect_with_deps` as effects are not executed during server side +rendering. + +::: warning + +Whilst it's possible to use Struct Components with server-side rendering, +There's no clear boundaries between client-side safe logic like the +`use_effect` hook for struct components. +Struct Components will continue to accept messages until all of its +children is rendered and `destroy` method is called. Developers need to +make sure no messages possibly passed to components would link to logic +that makes use of Web APIs. + +::: + +# Data Fetching during Server-side Rendering + +Data fetching is one of the difficult point with server side rendering +and hydration. Traditionally, when the application renders, it is +instantly available. So there's no mechanism for Yew to detect whether +the application is still fetching. Hence, data client is responsible to implement +a custom solution to detect what's being requested during initial +rendering and triggers a second render after requests are fulfilled. +During the hydration process, the data clients also need to provide a way +to make the data fetched on the server-side available during hydration. + +Yew takes a different approach by trying to solve this issue with ``. + +Suspense is a special component that when used on the client-side, +provides a way to show a fallback UI while the component is fetching +data (suspended) and resumes to normal UI when data fetching completes. + +When the application is rendered on the server-side, Yew waits until a +component is no longer suspended before serializing it to the string +buffer. + +Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/suspense) + +::: warning + +Server-side rendering is experiemental and currently has no hydration support. +However, you can still generate static websites. + +::: diff --git a/website/docs/concepts/suspense.md b/website/docs/concepts/suspense.md index a8ac666a65d..ad7e041be39 100644 --- a/website/docs/concepts/suspense.md +++ b/website/docs/concepts/suspense.md @@ -92,7 +92,7 @@ fn load_user() -> Option { todo!() // implementation omitted. } -fn on_load_user_complete(_fn: F) { +fn on_load_user_complete(_fn: F) { todo!() // implementation omitted. } diff --git a/website/sidebars.js b/website/sidebars.js index 0f5f8f5df58..7e0998e4c95 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -94,6 +94,7 @@ module.exports = { "advanced-topics/how-it-works", "advanced-topics/optimizations", "advanced-topics/portals", + "advanced-topics/server-side-rendering", ], }, { From 482865e660f7bf259e7011c15f5b2f68fa1c0604 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 01:42:19 +0900 Subject: [PATCH 08/27] Fix tests. --- examples/simple_ssr/README.md | 6 ++++++ packages/yew/src/virtual_dom/vtag.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 examples/simple_ssr/README.md diff --git a/examples/simple_ssr/README.md b/examples/simple_ssr/README.md new file mode 100644 index 00000000000..95cf18b43ea --- /dev/null +++ b/examples/simple_ssr/README.md @@ -0,0 +1,6 @@ +# Server-side Rendering Example + +This example demonstrates server-side rendering. + +Run `cargo run -p simple_ssr` and navigate to http://localhost:8080/ to +view results. diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 6c48b7b2984..b766a9bb8bf 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1543,7 +1543,7 @@ mod ssr_tests { let renderer = YewServerRenderer::::new(); - let s = renderer.render_to_string().await; + let s = renderer.render().await; assert_eq!(s, r#"
Hello!
"#); } From 10f4ac4c7d7614cd3bc5a4e929b9c80ab78a6ba7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 01:47:36 +0900 Subject: [PATCH 09/27] Fix docs. --- examples/simple_ssr/Cargo.toml | 2 +- website/docs/advanced-topics/server-side-rendering.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml index da8904cf2e4..df99f2e0b92 100644 --- a/examples/simple_ssr/Cargo.toml +++ b/examples/simple_ssr/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "simple_ssr" version = "0.1.0" -edition = "2021" +edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index 1bb0e07d934..7a43f568f82 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -65,7 +65,7 @@ You should isolate logics that need Web APIs in `use_effect` or `use_effect_with_deps` as effects are not executed during server side rendering. -::: warning +:::caution Whilst it's possible to use Struct Components with server-side rendering, There's no clear boundaries between client-side safe logic like the @@ -100,7 +100,7 @@ buffer. Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/suspense) -::: warning +:::caution Server-side rendering is experiemental and currently has no hydration support. However, you can still use it to generate static websites. From 77b48c0ce7a75f18471f406cd786a58d5526f995 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 01:57:19 +0900 Subject: [PATCH 10/27] Fix heading size. --- website/docs/advanced-topics/server-side-rendering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index 7a43f568f82..f3e6c23adda 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -77,7 +77,7 @@ that makes use of Web APIs. ::: -# Data Fetching during Server-side Rendering +## Data Fetching during Server-side Rendering Data fetching is one of the difficult point with server side rendering and hydration. Traditionally, when the application renders, it is From 476300a3b911e6970f55d130b7f51fc40298d0fd Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 02:02:07 +0900 Subject: [PATCH 11/27] Remove the unused YewRenderer. --- packages/yew/src/lib.rs | 60 ----------------------------------------- 1 file changed, 60 deletions(-) diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 3bac1cce100..f9d5e6a1a6c 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -304,66 +304,6 @@ fn set_default_panic_hook() { } } -// /// A Yew Renderer. -// #[derive(Debug)] -// pub struct YewRenderer -// where -// COMP: BaseComponent, -// { -// root: Element, -// props: COMP::Properties, -// } - -// impl YewRenderer -// where -// COMP: BaseComponent, -// { -// /// Creates a [`YewRenderer`] with a custom root and properties. -// pub fn with_root_and_props(root: Element, props: COMP::Properties) -> Self { -// Self { root, props } -// } - -// /// Creates a [`YewRenderer`] with document body as root and custom properties. -// pub fn with_props(props: COMP::Properties) -> Self { -// Self::with_root_and_props( -// gloo_utils::document() -// .body() -// .expect("no body node found") -// .into(), -// props, -// ) -// } - -// /// Renders a Yew application. -// pub fn render(self) -> AppHandle { -// set_default_panic_hook(); - -// AppHandle::::mount_with_props(self.root, Rc::new(self.props)) -// } - -// /// Hydrates a Yew application. -// pub fn hydrate(self) -> AppHandle { -// set_default_panic_hook(); -// todo!() -// } -// } - -// impl YewRenderer -// where -// COMP: BaseComponent, -// COMP::Properties: Default, -// { -// /// Creates a [`YewRenderer`] with a custom root. -// pub fn with_root(root: Element) -> Self { -// Self::with_root_and_props(root, COMP::Properties::default()) -// } - -// /// Creates a [`YewRenderer`] with document body as root. -// pub fn body() -> Self { -// Self::with_props(COMP::Properties::default()) -// } -// } - #[cfg(feature = "ssr")] mod feat_ssr { use super::*; From 95e1d39b541ce0ee3492fe60a9fcc9f7745a2865 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 02:07:24 +0900 Subject: [PATCH 12/27] Remove extra comment. --- examples/simple_ssr/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/simple_ssr/src/main.rs b/examples/simple_ssr/src/main.rs index 69391621b87..a62db0ee0dc 100644 --- a/examples/simple_ssr/src/main.rs +++ b/examples/simple_ssr/src/main.rs @@ -120,7 +120,6 @@ async fn render() -> String { #[tokio::main] async fn main() { - // Match any request and return hello world! let routes = warp::any().then(|| async move { warp::reply::html(render().await) }); println!("You can view the website at: http://localhost:8080/"); From 655afd4d1180167235276210625ee5ec5367e621 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 02:37:50 +0900 Subject: [PATCH 13/27] unify naming. --- packages/yew/src/html/component/scope.rs | 4 ++-- packages/yew/src/lib.rs | 2 +- packages/yew/src/virtual_dom/vcomp.rs | 10 +++++----- packages/yew/src/virtual_dom/vlist.rs | 4 ++-- packages/yew/src/virtual_dom/vnode.rs | 20 +++++++++++--------- packages/yew/src/virtual_dom/vsuspense.rs | 4 ++-- packages/yew/src/virtual_dom/vtag.rs | 6 +++--- packages/yew/src/virtual_dom/vtext.rs | 4 ++-- 8 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 4b950d9edaa..3903e6922c6 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -422,7 +422,7 @@ mod feat_ssr { use futures::channel::oneshot; impl Scope { - pub(crate) async fn render_to_html(&self, w: &mut String, props: Rc) { + pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc) { let (tx, rx) = oneshot::channel(); scheduler::push_component_create( @@ -447,7 +447,7 @@ mod feat_ssr { let html = rx.await.unwrap(); let self_any_scope = self.to_any(); - html.render_to_html(w, &self_any_scope).await; + html.render_to_string(w, &self_any_scope).await; scheduler::push_component_destroy(DestroyRunner { state: self.state.clone(), diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index f9d5e6a1a6c..6242360599f 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -361,7 +361,7 @@ mod feat_ssr { /// Renders Yew Application to a String. pub async fn render_to_string(self, w: &mut String) { let scope = Scope::::new(None); - scope.render_to_html(w, self.props.into()).await; + scope.render_to_string(w, self.props.into()).await; } } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index a338134b195..57afaef6349 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -185,7 +185,7 @@ trait Mountable { fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); #[cfg(feature = "ssr")] - fn render_to_html<'a>( + fn render_to_string<'a>( &'a self, w: &'a mut String, parent_scope: &'a AnyScope, @@ -229,14 +229,14 @@ impl Mountable for PropsWrapper { } #[cfg(feature = "ssr")] - fn render_to_html<'a>( + fn render_to_string<'a>( &'a self, w: &'a mut String, parent_scope: &'a AnyScope, ) -> LocalBoxFuture<'a, ()> { async move { let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.render_to_html(w, self.props.clone()).await; + scope.render_to_string(w, self.props.clone()).await; } .boxed_local() } @@ -313,12 +313,12 @@ mod feat_ssr { use super::*; impl VComp { - pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { self.mountable .as_ref() .map(|m| m.copy()) .unwrap() - .render_to_html(w, parent_scope) + .render_to_string(w, parent_scope) .await; } } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index e366d297fbb..b02a03e7765 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -289,9 +289,9 @@ mod feat_ssr { use super::*; impl VList { - pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { for node in self.children.iter() { - node.render_to_html(w, parent_scope).await; + node.render_to_string(w, parent_scope).await; } } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index a0e2f9ffc2a..cbbe3f3b8cc 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -303,28 +303,30 @@ mod feat_ssr { use super::*; impl VNode { - pub(crate) fn render_to_html<'a>( + pub(crate) fn render_to_string<'a>( &'a self, w: &'a mut String, parent_scope: &'a AnyScope, // we box here due to: https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html ) -> LocalBoxFuture<'a, ()> { async move { match self { - VNode::VTag(vtag) => vtag.render_to_html(w, parent_scope).await, - VNode::VText(vtext) => vtext.render_to_html(w).await, - VNode::VComp(vcomp) => vcomp.render_to_html(w, parent_scope).await, - VNode::VList(vlist) => vlist.render_to_html(w, parent_scope).await, + 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, // We are pretty safe here as it's not possible to get a web_sys::Node without DOM // support in the first place. // - // The only exception would be to use `YewServerRenderer` in a browser or wasm32 with - // jsdom present environment, in which case, it's uncharted territory. + // The only exception would be to use `YewServerRenderer` in a browser or wasm32 environment with + // jsdom present. VNode::VRef(_) => { - panic!("VRef is not possible to be rendered in to a html writer.") + panic!("VRef is not possible to be rendered in to a string.") } // Portals are not rendered. VNode::VPortal(_) => {} - VNode::VSuspense(vsuspense) => vsuspense.render_to_html(w, parent_scope).await, + VNode::VSuspense(vsuspense) => { + vsuspense.render_to_string(w, parent_scope).await + } } } .boxed_local() diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 0b679c2adca..1e00eccbba9 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -155,9 +155,9 @@ mod feat_ssr { use super::*; impl VSuspense { - pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { // always render children on the server side. - self.children.render_to_html(w, parent_scope).await; + self.children.render_to_string(w, parent_scope).await; } } } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index b766a9bb8bf..60c8645a8b7 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -644,7 +644,7 @@ mod feat_ssr { use std::fmt::Write; impl VTag { - pub(crate) async fn render_to_html(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { write!(w, "<{}", self.tag()).unwrap(); let write_attr = |w: &mut String, name: &str, val: Option<&str>| { @@ -675,7 +675,7 @@ mod feat_ssr { VTagInner::Input(_) => {} VTagInner::Textarea { .. } => { if let Some(m) = self.value() { - VText::new(m.to_owned()).render_to_html(w).await; + VText::new(m.to_owned()).render_to_string(w).await; } w.push_str(""); @@ -686,7 +686,7 @@ mod feat_ssr { .. } => { for child in children.iter() { - child.render_to_html(w, parent_scope).await; + child.render_to_string(w, parent_scope).await; } write!(w, "", tag).unwrap(); diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index ee5c8233866..4b458fccc34 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -33,7 +33,7 @@ mod feat_ssr { use super::*; impl VText { - pub(crate) async fn render_to_html(&self, w: &mut String) { + pub(crate) async fn render_to_string(&self, w: &mut String) { html_escape::encode_text_to_string(&self.text, w); } } @@ -204,7 +204,7 @@ mod ssr_tests { let mut s = String::new(); - vtext.render_to_html(&mut s).await; + vtext.render_to_string(&mut s).await; assert_eq!("abc", s.as_str()); } From 26bebb53a208991d92137db1bf8e34ec717995f5 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 13:22:24 +0900 Subject: [PATCH 14/27] Update docs. --- .../advanced-topics/server-side-rendering.md | 85 ++++++++++++------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index f3e6c23adda..b8a8931181e 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -5,18 +5,18 @@ description: "Render Yew on the server-side." # Server-side Rendering -By default, Yew applications renders at the client side. That is, a skeleton -html file without any actual content and a WebAssembly bundle is -downloaded to the browser and everything is rendered at the client side. -This is known as client-side rendering. +By default, Yew applications render at the client side. When the viewer +visits a website, the server sends a skeleton html file without any actual +content and a WebAssembly bundle is downloaded to the browser. +Everything is then rendered at the client side by the WebAssembly +bundle. This is known as client-side rendering. -This approach works fine for most websites as most users now use a modern -browser and devices with adequate computing power. - -However, there're some caveats with client-side rendering: +This approach works fine for most websites, with some caveats: 1. The user will not be able to see anything until the entire application is - downloaded and initial render has completed. + downloaded and initial render has completed. This can result in poor user + experience if the user is using a slow network and your application is + big. 2. Some search engines do not support dynamically rendered web content and those who do usually rank dynamic websites lower in the search results. @@ -24,12 +24,12 @@ To solve these problems, we can render our website on the server side. ## How it Works -Yew provides a `YewServerRenderer` renderer to render pages on the +Yew provides a `YewServerRenderer` to render pages on the server-side. -You can create a render with `YewServerRenderer::::new()`. -And calling `renderer.render().await` will render `` -into a `String`. +To render Yew components at the server-side, you can create a renderer +with `YewServerRenderer::::new()` and call `renderer.render().await` +to render `` into a `String`. ```rust use yew::prelude::*; @@ -56,8 +56,11 @@ async fn main() { The recommended way of working with server-side rendering is function components. -All hooks other than `use_effect` will function normally until a component -successfully renders into `Html` for the first time. +All hooks other than `use_effect` (and `use_effect_with_deps`) +will function normally until a component successfully renders into `Html` +for the first time. + +:::caution Web APIs are not available! Web APIs such as `web_sys` are not available when using server-side rendering. You application will panic if you try to use them. @@ -65,13 +68,17 @@ You should isolate logics that need Web APIs in `use_effect` or `use_effect_with_deps` as effects are not executed during server side rendering. -:::caution +::: + +:::danger Whilst it's possible to use Struct Components with server-side rendering, -There's no clear boundaries between client-side safe logic like the -`use_effect` hook for struct components. -Struct Components will continue to accept messages until all of its -children is rendered and `destroy` method is called. Developers need to +there's no clear boundaries between client-side safe logic like the +`use_effect` hook for struct components and lifecycle events are invoked +in a different order than client side. + +In addition, Struct Components will continue to accept messages until all of its +children are rendered and `destroy` method is called. Developers need to make sure no messages possibly passed to components would link to logic that makes use of Web APIs. @@ -80,25 +87,43 @@ that makes use of Web APIs. ## Data Fetching during Server-side Rendering Data fetching is one of the difficult point with server side rendering -and hydration. Traditionally, when the application renders, it is -instantly available. So there's no mechanism for Yew to detect whether -the application is still fetching. Hence, data client is responsible to implement -a custom solution to detect what's being requested during initial -rendering and triggers a second render after requests are fulfilled. -During the hydration process, the data clients also need to provide a way -to make the data fetched on the server-side available during hydration. +and hydration. + +Traditionally, when a component renders, it is instantly available +(outputs a virtual dom to be rendered). This works fine when if the +component does not want to fetch any data. But what happens if the component +wants to fetch some data during rendering? + +In the past, there's no mechanism for Yew to detect whether a component is still +fetching data or ready. The data fetching client is responsible to implement +a solution to detect what's being requested during initial render and triggers +a second render after requests are fulfilled. The server repeats this process until +no more pending requests are added during a render before returning a response. + +Not only this wastes CPU resources by repeatedly rendering components, +but the data client also needs to provide a way to make the data fetched on +the server-side available during hydration process to make sure that the +the virtual dom returned by initial render is consistent with the +server-side rendered DOM tree which can be hard to implement. Yew takes a different approach by trying to solve this issue with ``. Suspense is a special component that when used on the client-side, provides a way to show a fallback UI while the component is fetching -data (suspended) and resumes to normal UI when data fetching completes. +data (suspended) and resumes to normal UI when the data fetching completes. When the application is rendered on the server-side, Yew waits until a -component is no longer suspended before serializing it to the string +component is no longer suspended before serializing it into the string buffer. -Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/suspense) +During the hydration process, elements within a ` component +remains dehydrated until all of its child components are no longer +suspended. + +With this approach, developers can build a client-agnostic, SSR ready +application with data fetching with very little effort. + +Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/simple_ssr) :::caution From 61f44d12d12c5a1a39d571190fd8b2df4b40e1d5 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 13:51:36 +0900 Subject: [PATCH 15/27] Update docs. --- .../advanced-topics/server-side-rendering.md | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index b8a8931181e..cd49c60df99 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -5,18 +5,17 @@ description: "Render Yew on the server-side." # Server-side Rendering -By default, Yew applications render at the client side. When the viewer +By default, Yew components render at the client side. When a viewer visits a website, the server sends a skeleton html file without any actual -content and a WebAssembly bundle is downloaded to the browser. -Everything is then rendered at the client side by the WebAssembly +content and a WebAssembly bundle to the browser. +Everything is rendered at the client side by the WebAssembly bundle. This is known as client-side rendering. This approach works fine for most websites, with some caveats: -1. The user will not be able to see anything until the entire application is - downloaded and initial render has completed. This can result in poor user - experience if the user is using a slow network and your application is - big. +1. Users will not be able to see anything until the entire WebAssembly + bundle is downloaded and initial render has completed. + This can result in poor user experience if the user is using a slow network. 2. Some search engines do not support dynamically rendered web content and those who do usually rank dynamic websites lower in the search results. @@ -62,15 +61,16 @@ for the first time. :::caution Web APIs are not available! -Web APIs such as `web_sys` are not available when using server-side rendering. -You application will panic if you try to use them. +Web APIs such as `web_sys` are not available when your component is +rendering on the server-side. +Your application will panic if you try to use them. You should isolate logics that need Web APIs in `use_effect` or `use_effect_with_deps` as effects are not executed during server side rendering. ::: -:::danger +:::danger Struct Components are not supported! Whilst it's possible to use Struct Components with server-side rendering, there's no clear boundaries between client-side safe logic like the @@ -82,6 +82,9 @@ children are rendered and `destroy` method is called. Developers need to make sure no messages possibly passed to components would link to logic that makes use of Web APIs. +When designing an application with server-side rendering support, +prefer function components unless you have a good reason not to. + ::: ## Data Fetching during Server-side Rendering @@ -90,7 +93,7 @@ Data fetching is one of the difficult point with server side rendering and hydration. Traditionally, when a component renders, it is instantly available -(outputs a virtual dom to be rendered). This works fine when if the +(outputs a virtual dom to be rendered). This works fine when the component does not want to fetch any data. But what happens if the component wants to fetch some data during rendering? @@ -116,7 +119,7 @@ When the application is rendered on the server-side, Yew waits until a component is no longer suspended before serializing it into the string buffer. -During the hydration process, elements within a ` component +During the hydration process, elements within a `` component remains dehydrated until all of its child components are no longer suspended. From 826b43154485de4ecdf8725b15cb101a3cf926e0 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 14:16:56 +0900 Subject: [PATCH 16/27] Update docs. --- website/docs/advanced-topics/server-side-rendering.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index cd49c60df99..92fba359711 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -70,11 +70,11 @@ rendering. ::: -:::danger Struct Components are not supported! +:::danger Struct Components Whilst it's possible to use Struct Components with server-side rendering, there's no clear boundaries between client-side safe logic like the -`use_effect` hook for struct components and lifecycle events are invoked +`use_effect` hook for function components and lifecycle events are invoked in a different order than client side. In addition, Struct Components will continue to accept messages until all of its @@ -98,7 +98,7 @@ component does not want to fetch any data. But what happens if the component wants to fetch some data during rendering? In the past, there's no mechanism for Yew to detect whether a component is still -fetching data or ready. The data fetching client is responsible to implement +fetching data. The data fetching client is responsible to implement a solution to detect what's being requested during initial render and triggers a second render after requests are fulfilled. The server repeats this process until no more pending requests are added during a render before returning a response. @@ -106,7 +106,7 @@ no more pending requests are added during a render before returning a response. Not only this wastes CPU resources by repeatedly rendering components, but the data client also needs to provide a way to make the data fetched on the server-side available during hydration process to make sure that the -the virtual dom returned by initial render is consistent with the +virtual dom returned by initial render is consistent with the server-side rendered DOM tree which can be hard to implement. Yew takes a different approach by trying to solve this issue with ``. From ed138b172cfd4e0a47ba6c8b9348a61605762f02 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 21:40:19 +0900 Subject: [PATCH 17/27] Isolate spawn_local. --- packages/yew/Cargo.toml | 13 ++- packages/yew/src/html/component/scope.rs | 123 ++++++++++++----------- packages/yew/src/io_coop.rs | 27 +++++ packages/yew/src/lib.rs | 1 + packages/yew/src/suspense/suspension.rs | 33 +++--- 5 files changed, 126 insertions(+), 71 deletions(-) create mode 100644 packages/yew/src/io_coop.rs diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 2e1f2c0ccca..7f0436fc304 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -23,7 +23,6 @@ indexmap = { version = "1", features = ["std"] } js-sys = "0.3" slab = "0.4" wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" yew-macro = { version = "^0.19.0", path = "../yew-macro" } thiserror = "1.0" @@ -63,16 +62,28 @@ features = [ "Window", ] +[target.'cfg(target_arch = "wasm32")'.dependencies] +# we move it here so no promise-based spawn_local can present for +# non-wasm32 targets. +wasm-bindgen-futures = "0.4" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1.15.0", features = ["rt"], optional = true } + [dev-dependencies] easybench-wasm = "0.2" wasm-bindgen-test = "0.3" gloo = { version = "0.4", features = ["futures"] } +wasm-bindgen-futures = "0.4" [features] doc_test = [] wasm_test = [] wasm_bench = [] ssr = ["futures", "html-escape"] +# we enable tokio by default so spawn_local is available on non-wasm targets +# by default as well. +default = ["tokio"] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.15.0", features = ["full"] } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 3903e6922c6..f63ab4c3c02 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -15,11 +15,9 @@ use crate::virtual_dom::{insert_node, VNode}; use gloo_utils::document; use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell}; -use std::future::Future; use std::ops::Deref; use std::rc::Rc; use std::{fmt, iter}; -use wasm_bindgen_futures::spawn_local; use web_sys::{Element, Node}; /// Untyped scope used for accessing parent scope @@ -350,61 +348,6 @@ impl Scope { }; closure.into() } - /// This method creates a [`Callback`] which returns a Future which - /// returns a message to be sent back to the component's event - /// loop. - /// - /// # Panics - /// If the future panics, then the promise will not resolve, and - /// will leak. - pub fn callback_future(&self, function: FN) -> Callback - where - M: Into, - FU: Future + 'static, - FN: Fn(IN) -> FU + 'static, - { - let link = self.clone(); - - let closure = move |input: IN| { - let future: FU = function(input); - link.send_future(future); - }; - - closure.into() - } - - /// This method processes a Future that returns a message and sends it back to the component's - /// loop. - /// - /// # Panics - /// If the future panics, then the promise will not resolve, and will leak. - pub fn send_future(&self, future: F) - where - M: Into, - F: Future + 'static, - { - let link = self.clone(); - let js_future = async move { - let message: COMP::Message = future.await.into(); - link.send_message(message); - }; - spawn_local(js_future); - } - - /// Registers a Future that resolves to multiple messages. - /// # Panics - /// If the future panics, then the promise will not resolve, and will leak. - pub fn send_future_batch(&self, future: F) - where - F: Future> + 'static, - { - let link = self.clone(); - let js_future = async move { - let messages: Vec = future.await; - link.send_message_batch(messages); - }; - spawn_local(js_future); - } /// Accesses a value provided by a parent `ContextProvider` component of the /// same type. @@ -457,6 +400,72 @@ mod feat_ssr { } } +#[cfg(any(target_arch = "wasm32", feature = "tokio"))] +mod feat_io { + use std::future::Future; + + use super::*; + use crate::io_coop::spawn_local; + + impl Scope { + /// This method creates a [`Callback`] which returns a Future which + /// returns a message to be sent back to the component's event + /// loop. + /// + /// # Panics + /// If the future panics, then the promise will not resolve, and + /// will leak. + pub fn callback_future(&self, function: FN) -> Callback + where + M: Into, + FU: Future + 'static, + FN: Fn(IN) -> FU + 'static, + { + let link = self.clone(); + + let closure = move |input: IN| { + let future: FU = function(input); + link.send_future(future); + }; + + closure.into() + } + + /// This method processes a Future that returns a message and sends it back to the component's + /// loop. + /// + /// # Panics + /// If the future panics, then the promise will not resolve, and will leak. + pub fn send_future(&self, future: F) + where + M: Into, + F: Future + 'static, + { + let link = self.clone(); + let js_future = async move { + let message: COMP::Message = future.await.into(); + link.send_message(message); + }; + spawn_local(js_future); + } + + /// Registers a Future that resolves to multiple messages. + /// # Panics + /// If the future panics, then the promise will not resolve, and will leak. + pub fn send_future_batch(&self, future: F) + where + F: Future> + 'static, + { + let link = self.clone(); + let js_future = async move { + let messages: Vec = future.await; + link.send_message_batch(messages); + }; + spawn_local(js_future); + } + } +} + /// Defines a message type that can be sent to a component. /// Used for the return value of closure given to [Scope::batch_callback](struct.Scope.html#method.batch_callback). pub trait SendAsMessage { diff --git a/packages/yew/src/io_coop.rs b/packages/yew/src/io_coop.rs new file mode 100644 index 00000000000..17df422f3f1 --- /dev/null +++ b/packages/yew/src/io_coop.rs @@ -0,0 +1,27 @@ +//! module that provides io compatibility over browser tasks and other async io tasks (e.g.: tokio) + +#[cfg(target_arch = "wasm32")] +mod io_wasm_bindgen { + pub use wasm_bindgen_futures::spawn_local; +} + +#[cfg(target_arch = "wasm32")] +pub(crate) use io_wasm_bindgen::*; + +#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] +mod io_tokio { + use std::future::Future; + + // spawn_local in tokio is more powerful, but we need to adjust the function signature to match + // wasm_bindgen_futures. + #[inline(always)] + pub(crate) fn spawn_local(f: F) + where + F: Future + 'static, + { + tokio::task::spawn_local(f); + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] +pub(crate) use io_tokio::*; diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 6242360599f..a0aef032ab2 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -260,6 +260,7 @@ pub mod callback; pub mod context; pub mod functional; pub mod html; +mod io_coop; pub mod scheduler; mod sealed; pub mod suspense; diff --git a/packages/yew/src/suspense/suspension.rs b/packages/yew/src/suspense/suspension.rs index 9430e8d6dd5..cbb35d19b9f 100644 --- a/packages/yew/src/suspense/suspension.rs +++ b/packages/yew/src/suspense/suspension.rs @@ -6,7 +6,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::task::{Context, Poll}; use thiserror::Error; -use wasm_bindgen_futures::spawn_local; use crate::Callback; @@ -52,18 +51,6 @@ impl Suspension { (self_.clone(), SuspensionHandle { inner: self_ }) } - /// Creates a Suspension that resumes when the [`Future`] resolves. - pub fn from_future(f: impl Future + 'static) -> Self { - let (self_, handle) = Self::new(); - - spawn_local(async move { - f.await; - handle.resume(); - }); - - self_ - } - /// Returns `true` if the current suspension is already resumed. pub fn resumed(&self) -> bool { self.resumed.load(Ordering::Relaxed) @@ -138,3 +125,23 @@ impl Drop for SuspensionHandle { self.inner.resume_by_ref(); } } + +#[cfg(any(target_arch = "wasm32", feature = "tokio"))] +mod feat_io { + use super::*; + use crate::io_coop::spawn_local; + + impl Suspension { + /// Creates a Suspension that resumes when the [`Future`] resolves. + pub fn from_future(f: impl Future + 'static) -> Self { + let (self_, handle) = Self::new(); + + spawn_local(async move { + f.await; + handle.resume(); + }); + + self_ + } + } +} From bd26db48555ce71915c64047970daad428cf83f9 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 21:49:12 +0900 Subject: [PATCH 18/27] Add doc flags. --- packages/yew/Cargo.toml | 1 + packages/yew/src/html/component/scope.rs | 2 +- packages/yew/src/lib.rs | 2 ++ packages/yew/src/suspense/suspension.rs | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 7f0436fc304..be9af34508c 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -90,3 +90,4 @@ tokio = { version = "1.15.0", features = ["full"] } [package.metadata.docs.rs] features = ["doc_test"] +rustdoc-args = ["--cfg", "documenting"] diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index f63ab4c3c02..70d0ce66410 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -399,7 +399,7 @@ mod feat_ssr { } } } - +#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] #[cfg(any(target_arch = "wasm32", feature = "tokio"))] mod feat_io { use std::future::Future; diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index a0aef032ab2..6df42022a35 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::needless_doctest_main)] #![doc(html_logo_url = "https://yew.rs/img/logo.png")] +#![cfg_attr(documenting, feature(doc_cfg))] //! # Yew Framework - API Documentation //! @@ -312,6 +313,7 @@ mod feat_ssr { use crate::html::Scope; /// A Yew Server-side Renderer. + #[cfg_attr(documenting, doc(cfg(feature = "ssr")))] #[derive(Debug)] pub struct YewServerRenderer where diff --git a/packages/yew/src/suspense/suspension.rs b/packages/yew/src/suspense/suspension.rs index cbb35d19b9f..501ab5c20f3 100644 --- a/packages/yew/src/suspense/suspension.rs +++ b/packages/yew/src/suspense/suspension.rs @@ -126,6 +126,7 @@ impl Drop for SuspensionHandle { } } +#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] #[cfg(any(target_arch = "wasm32", feature = "tokio"))] mod feat_io { use super::*; From f003aabd6167a0bbe7dc4c4dea6aa36a354a2c36 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 7 Jan 2022 21:49:54 +0900 Subject: [PATCH 19/27] Add ssr feature to docs. --- packages/yew/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index be9af34508c..45caf4fcaae 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -89,5 +89,5 @@ default = ["tokio"] tokio = { version = "1.15.0", features = ["full"] } [package.metadata.docs.rs] -features = ["doc_test"] +features = ["doc_test", "ssr"] rustdoc-args = ["--cfg", "documenting"] From e56fcef10fd742b17b35861b6f057220f98c1149 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 8 Jan 2022 02:05:58 +0900 Subject: [PATCH 20/27] Move ServerRenderer into their own file. --- examples/simple_ssr/src/main.rs | 2 +- packages/yew/src/lib.rs | 70 ++--------------------- packages/yew/src/server_renderer.rs | 59 +++++++++++++++++++ packages/yew/src/virtual_dom/vcomp.rs | 4 +- packages/yew/src/virtual_dom/vlist.rs | 6 +- packages/yew/src/virtual_dom/vnode.rs | 2 +- packages/yew/src/virtual_dom/vsuspense.rs | 4 +- packages/yew/src/virtual_dom/vtag.rs | 12 ++-- 8 files changed, 78 insertions(+), 81 deletions(-) create mode 100644 packages/yew/src/server_renderer.rs diff --git a/examples/simple_ssr/src/main.rs b/examples/simple_ssr/src/main.rs index a62db0ee0dc..b5f75aca993 100644 --- a/examples/simple_ssr/src/main.rs +++ b/examples/simple_ssr/src/main.rs @@ -95,7 +95,7 @@ async fn render() -> String { let rt = Builder::new_current_thread().enable_all().build().unwrap(); set.block_on(&rt, async { - let renderer = yew::YewServerRenderer::::new(); + let renderer = yew::ServerRenderer::::new(); renderer.render().await }) diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 6df42022a35..c3385da56ab 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -264,11 +264,15 @@ pub mod html; mod io_coop; pub mod scheduler; mod sealed; +#[cfg(feature = "ssr")] +mod server_renderer; pub mod suspense; #[cfg(test)] pub mod tests; pub mod utils; pub mod virtual_dom; +#[cfg(feature = "ssr")] +pub use server_renderer::*; /// The module that contains all events available in the framework. pub mod events { @@ -306,72 +310,6 @@ fn set_default_panic_hook() { } } -#[cfg(feature = "ssr")] -mod feat_ssr { - use super::*; - - use crate::html::Scope; - - /// A Yew Server-side Renderer. - #[cfg_attr(documenting, doc(cfg(feature = "ssr")))] - #[derive(Debug)] - pub struct YewServerRenderer - where - COMP: BaseComponent, - { - props: COMP::Properties, - } - - impl Default for YewServerRenderer - where - COMP: BaseComponent, - COMP::Properties: Default, - { - fn default() -> Self { - Self::with_props(COMP::Properties::default()) - } - } - - impl YewServerRenderer - where - COMP: BaseComponent, - COMP::Properties: Default, - { - /// Creates a [`YewServerRenderer`] with default properties. - pub fn new() -> Self { - Self::default() - } - } - - impl YewServerRenderer - where - COMP: BaseComponent, - { - /// Creates a [`YewServerRenderer`] with custom properties. - pub fn with_props(props: COMP::Properties) -> Self { - Self { props } - } - - /// Renders Yew Application. - pub async fn render(self) -> String { - let mut s = String::new(); - - self.render_to_string(&mut s).await; - - s - } - - /// Renders Yew Application to a String. - pub async fn render_to_string(self, w: &mut String) { - let scope = Scope::::new(None); - scope.render_to_string(w, self.props.into()).await; - } - } -} - -#[cfg(feature = "ssr")] -pub use feat_ssr::*; - /// The main entry point of a Yew application. /// If you would like to pass props, use the `start_app_with_props_in_element` method. pub fn start_app_in_element(element: Element) -> AppHandle diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs new file mode 100644 index 00000000000..9e5cd5fe1cb --- /dev/null +++ b/packages/yew/src/server_renderer.rs @@ -0,0 +1,59 @@ +use super::*; + +use crate::html::Scope; + +/// A Yew Server-side Renderer. +#[cfg_attr(documenting, doc(cfg(feature = "ssr")))] +#[derive(Debug)] +pub struct ServerRenderer +where + COMP: BaseComponent, +{ + props: COMP::Properties, +} + +impl Default for ServerRenderer +where + COMP: BaseComponent, + COMP::Properties: Default, +{ + fn default() -> Self { + Self::with_props(COMP::Properties::default()) + } +} + +impl ServerRenderer +where + COMP: BaseComponent, + COMP::Properties: Default, +{ + /// Creates a [ServerRenderer] with default properties. + pub fn new() -> Self { + Self::default() + } +} + +impl ServerRenderer +where + COMP: BaseComponent, +{ + /// Creates a [ServerRenderer] with custom properties. + pub fn with_props(props: COMP::Properties) -> Self { + Self { props } + } + + /// Renders Yew Application. + pub async fn render(self) -> String { + let mut s = String::new(); + + self.render_to_string(&mut s).await; + + s + } + + /// Renders Yew Application to a String. + pub async fn render_to_string(self, w: &mut String) { + let scope = Scope::::new(None); + scope.render_to_string(w, self.props.into()).await; + } +} diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 57afaef6349..32b921f3863 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -914,7 +914,7 @@ mod ssr_tests { use tokio::test; use crate::prelude::*; - use crate::YewServerRenderer; + use crate::ServerRenderer; #[test] async fn test_props() { @@ -939,7 +939,7 @@ mod ssr_tests { } } - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); let s = renderer.render().await; diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index b02a03e7765..338d67cdf05 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1286,7 +1286,7 @@ mod ssr_tests { use tokio::test; use crate::prelude::*; - use crate::YewServerRenderer; + use crate::ServerRenderer; #[test] async fn test_text_back_to_back() { @@ -1297,7 +1297,7 @@ mod ssr_tests { html! {
{"Hello "}{s}{"!"}
} } - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); let s = renderer.render().await; @@ -1327,7 +1327,7 @@ mod ssr_tests { } } - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); let s = renderer.render().await; diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index cbbe3f3b8cc..1fa5e73e7ec 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -317,7 +317,7 @@ mod feat_ssr { // We are pretty safe here as it's not possible to get a web_sys::Node without DOM // support in the first place. // - // The only exception would be to use `YewServerRenderer` in a browser or wasm32 environment with + // The only exception would be to use `ServerRenderer` in a browser or wasm32 environment with // jsdom present. VNode::VRef(_) => { panic!("VRef is not possible to be rendered in to a string.") diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 1e00eccbba9..e2a73bb1446 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -173,7 +173,7 @@ mod ssr_tests { use crate::prelude::*; use crate::suspense::{Suspension, SuspensionResult}; - use crate::YewServerRenderer; + use crate::ServerRenderer; #[test(flavor = "multi_thread", worker_threads = 2)] async fn test_suspense() { @@ -244,7 +244,7 @@ mod ssr_tests { let s = local .run_until(async move { - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); renderer.render().await }) diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 60c8645a8b7..28feb26351d 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1504,7 +1504,7 @@ mod ssr_tests { use tokio::test; use crate::prelude::*; - use crate::YewServerRenderer; + use crate::ServerRenderer; #[test] async fn test_simple_tag() { @@ -1513,7 +1513,7 @@ mod ssr_tests { html! {
} } - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); let s = renderer.render().await; @@ -1527,7 +1527,7 @@ mod ssr_tests { html! {
} } - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); let s = renderer.render().await; @@ -1541,7 +1541,7 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); let s = renderer.render().await; @@ -1555,7 +1555,7 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = YewServerRenderer::::new(); + let renderer = ServerRenderer::::new(); let s = renderer.render().await; @@ -1569,7 +1569,7 @@ mod ssr_tests { html! {