mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Introduce additional information in SSR artifact to facilitate Hydration (#2540)
* Bring changes to this branch. * Child components always render after parents. * Add hydratable to render_to_string. * Revert portal example. * Fix vcomp. * Prefer debug_assert. * ServerRenderer now a Builder Pattern. * Collectable.
This commit is contained in:
parent
ee6a67e3ea
commit
2e098f4f6c
@ -204,8 +204,15 @@ mod feat_ssr {
|
||||
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
|
||||
};
|
||||
|
||||
use crate::virtual_dom::Collectable;
|
||||
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
pub(crate) async fn render_to_string(self, w: &mut String, props: Rc<COMP::Properties>) {
|
||||
pub(crate) async fn render_to_string(
|
||||
self,
|
||||
w: &mut String,
|
||||
props: Rc<COMP::Properties>,
|
||||
hydratable: bool,
|
||||
) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let state = ComponentRenderState::Ssr { sender: Some(tx) };
|
||||
|
||||
@ -222,10 +229,24 @@ mod feat_ssr {
|
||||
);
|
||||
scheduler::start();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let collectable = Collectable::Component(std::any::type_name::<COMP>());
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let collectable = Collectable::Component;
|
||||
|
||||
if hydratable {
|
||||
collectable.write_open_tag(w);
|
||||
}
|
||||
|
||||
let html = rx.await.unwrap();
|
||||
|
||||
let self_any_scope = AnyScope::from(self.clone());
|
||||
html.render_to_string(w, &self_any_scope).await;
|
||||
html.render_to_string(w, &self_any_scope, hydratable).await;
|
||||
|
||||
if hydratable {
|
||||
collectable.write_close_tag(w);
|
||||
}
|
||||
|
||||
scheduler::push_component_destroy(Box::new(DestroyRunner {
|
||||
state: self.state.clone(),
|
||||
|
||||
@ -10,6 +10,7 @@ where
|
||||
ICOMP: IntoComponent,
|
||||
{
|
||||
props: ICOMP::Properties,
|
||||
hydratable: bool,
|
||||
}
|
||||
|
||||
impl<ICOMP> Default for ServerRenderer<ICOMP>
|
||||
@ -39,7 +40,22 @@ where
|
||||
{
|
||||
/// Creates a [ServerRenderer] with custom properties.
|
||||
pub fn with_props(props: ICOMP::Properties) -> Self {
|
||||
Self { props }
|
||||
Self {
|
||||
props,
|
||||
hydratable: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets whether an the rendered result is hydratable.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
///
|
||||
/// When this is sets to `true`, the rendered artifact will include additional information
|
||||
/// to assist with the hydration process.
|
||||
pub fn hydratable(mut self, val: bool) -> Self {
|
||||
self.hydratable = val;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Renders Yew Application.
|
||||
@ -54,6 +70,8 @@ where
|
||||
/// Renders Yew Application to a String.
|
||||
pub async fn render_to_string(self, w: &mut String) {
|
||||
let scope = Scope::<<ICOMP as IntoComponent>::Component>::new(None);
|
||||
scope.render_to_string(w, self.props.into()).await;
|
||||
scope
|
||||
.render_to_string(w, self.props.into(), self.hydratable)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,6 +179,84 @@ mod tests_attr_value {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")] // & feature = "hydration"
|
||||
mod feat_ssr_hydration {
|
||||
/// A collectable.
|
||||
///
|
||||
/// This indicates a kind that can be collected from fragment to be processed at a later time
|
||||
pub(crate) enum Collectable {
|
||||
#[cfg(debug_assertions)]
|
||||
Component(&'static str),
|
||||
#[cfg(not(debug_assertions))]
|
||||
Component,
|
||||
Suspense,
|
||||
}
|
||||
|
||||
impl Collectable {
|
||||
pub fn open_start_mark(&self) -> &'static str {
|
||||
match self {
|
||||
#[cfg(debug_assertions)]
|
||||
Self::Component(_) => "<[",
|
||||
#[cfg(not(debug_assertions))]
|
||||
Self::Component => "<[",
|
||||
Self::Suspense => "<?",
|
||||
}
|
||||
}
|
||||
pub fn close_start_mark(&self) -> &'static str {
|
||||
match self {
|
||||
#[cfg(debug_assertions)]
|
||||
Self::Component(_) => "</[",
|
||||
#[cfg(not(debug_assertions))]
|
||||
Self::Component => "</[",
|
||||
Self::Suspense => "</?",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_mark(&self) -> &'static str {
|
||||
match self {
|
||||
#[cfg(debug_assertions)]
|
||||
Self::Component(_) => "]>",
|
||||
#[cfg(not(debug_assertions))]
|
||||
Self::Component => "]>",
|
||||
Self::Suspense => ">",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn write_open_tag(&self, w: &mut String) {
|
||||
w.push_str("<!--");
|
||||
w.push_str(self.open_start_mark());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
match self {
|
||||
Self::Component(type_name) => w.push_str(type_name),
|
||||
Self::Suspense => {}
|
||||
}
|
||||
|
||||
w.push_str(self.end_mark());
|
||||
w.push_str("-->");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn write_close_tag(&self, w: &mut String) {
|
||||
w.push_str("<!--");
|
||||
w.push_str(self.close_start_mark());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
match self {
|
||||
Self::Component(type_name) => w.push_str(type_name),
|
||||
Self::Suspense => {}
|
||||
}
|
||||
|
||||
w.push_str(self.end_mark());
|
||||
w.push_str("-->");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) use feat_ssr_hydration::*;
|
||||
|
||||
/// A collection of attributes for an element
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub enum Attributes {
|
||||
|
||||
@ -70,6 +70,7 @@ pub(crate) trait Mountable {
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
hydratable: bool,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
}
|
||||
|
||||
@ -117,10 +118,13 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
hydratable: bool,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.render_to_string(w, self.props.clone()).await;
|
||||
scope
|
||||
.render_to_string(w, self.props.clone(), hydratable)
|
||||
.await;
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
@ -210,10 +214,15 @@ mod feat_ssr {
|
||||
use crate::html::AnyScope;
|
||||
|
||||
impl VComp {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
pub(crate) async fn render_to_string(
|
||||
&self,
|
||||
w: &mut String,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
) {
|
||||
self.mountable
|
||||
.as_ref()
|
||||
.render_to_string(w, parent_scope)
|
||||
.render_to_string(w, parent_scope, hydratable)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,12 +90,17 @@ mod feat_ssr {
|
||||
use crate::html::AnyScope;
|
||||
|
||||
impl VList {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
pub(crate) async fn render_to_string(
|
||||
&self,
|
||||
w: &mut String,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
) {
|
||||
// Concurrently render all children.
|
||||
for fragment in futures::future::join_all(self.children.iter().map(|m| async move {
|
||||
let mut w = String::new();
|
||||
|
||||
m.render_to_string(&mut w, parent_scope).await;
|
||||
m.render_to_string(&mut w, parent_scope, hydratable).await;
|
||||
|
||||
w
|
||||
}))
|
||||
@ -123,9 +128,10 @@ mod ssr_tests {
|
||||
html! { <div>{"Hello "}{s}{"!"}</div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, "<div>Hello world!</div>");
|
||||
}
|
||||
@ -153,9 +159,10 @@ mod ssr_tests {
|
||||
}
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
|
||||
@ -157,13 +157,20 @@ mod feat_ssr {
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
hydratable: bool,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
match self {
|
||||
VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope).await,
|
||||
VNode::VText(vtext) => vtext.render_to_string(w).await,
|
||||
VNode::VComp(vcomp) => vcomp.render_to_string(w, parent_scope).await,
|
||||
VNode::VList(vlist) => vlist.render_to_string(w, parent_scope).await,
|
||||
VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope, hydratable).await,
|
||||
VNode::VText(vtext) => {
|
||||
vtext.render_to_string(w, parent_scope, hydratable).await
|
||||
}
|
||||
VNode::VComp(vcomp) => {
|
||||
vcomp.render_to_string(w, parent_scope, hydratable).await
|
||||
}
|
||||
VNode::VList(vlist) => {
|
||||
vlist.render_to_string(w, parent_scope, hydratable).await
|
||||
}
|
||||
// We are pretty safe here as it's not possible to get a web_sys::Node without DOM
|
||||
// support in the first place.
|
||||
//
|
||||
@ -175,7 +182,9 @@ mod feat_ssr {
|
||||
// Portals are not rendered.
|
||||
VNode::VPortal(_) => {}
|
||||
VNode::VSuspense(vsuspense) => {
|
||||
vsuspense.render_to_string(w, parent_scope).await
|
||||
vsuspense
|
||||
.render_to_string(w, parent_scope, hydratable)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,11 +28,29 @@ impl VSuspense {
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use crate::html::AnyScope;
|
||||
use crate::virtual_dom::Collectable;
|
||||
|
||||
impl VSuspense {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
pub(crate) async fn render_to_string(
|
||||
&self,
|
||||
w: &mut String,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
) {
|
||||
let collectable = Collectable::Suspense;
|
||||
|
||||
if hydratable {
|
||||
collectable.write_open_tag(w);
|
||||
}
|
||||
|
||||
// always render children on the server side.
|
||||
self.children.render_to_string(w, parent_scope).await;
|
||||
self.children
|
||||
.render_to_string(w, parent_scope, hydratable)
|
||||
.await;
|
||||
|
||||
if hydratable {
|
||||
collectable.write_close_tag(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -120,9 +138,10 @@ mod ssr_tests {
|
||||
|
||||
let s = local
|
||||
.run_until(async move {
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
renderer.render().await
|
||||
ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@ -418,8 +418,19 @@ mod feat_ssr {
|
||||
use crate::{html::AnyScope, virtual_dom::VText};
|
||||
use std::fmt::Write;
|
||||
|
||||
// Elements that cannot have any child elements.
|
||||
static VOID_ELEMENTS: &[&str; 14] = &[
|
||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
|
||||
"source", "track", "wbr",
|
||||
];
|
||||
|
||||
impl VTag {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
pub(crate) async fn render_to_string(
|
||||
&self,
|
||||
w: &mut String,
|
||||
parent_scope: &AnyScope,
|
||||
hydratable: bool,
|
||||
) {
|
||||
write!(w, "<{}", self.tag()).unwrap();
|
||||
|
||||
let write_attr = |w: &mut String, name: &str, val: Option<&str>| {
|
||||
@ -450,7 +461,9 @@ mod feat_ssr {
|
||||
VTagInner::Input(_) => {}
|
||||
VTagInner::Textarea { .. } => {
|
||||
if let Some(m) = self.value() {
|
||||
VText::new(m.to_owned()).render_to_string(w).await;
|
||||
VText::new(m.to_owned())
|
||||
.render_to_string(w, parent_scope, hydratable)
|
||||
.await;
|
||||
}
|
||||
|
||||
w.push_str("</textarea>");
|
||||
@ -460,9 +473,14 @@ mod feat_ssr {
|
||||
ref children,
|
||||
..
|
||||
} => {
|
||||
children.render_to_string(w, parent_scope).await;
|
||||
if !VOID_ELEMENTS.contains(&tag.as_ref()) {
|
||||
children.render_to_string(w, parent_scope, hydratable).await;
|
||||
|
||||
write!(w, "</{}>", tag).unwrap();
|
||||
} else {
|
||||
// We don't write children of void elements nor closing tags.
|
||||
debug_assert!(children.is_empty(), "{} cannot have any children!", tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -483,9 +501,10 @@ mod ssr_tests {
|
||||
html! { <div></div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, "<div></div>");
|
||||
}
|
||||
@ -497,9 +516,10 @@ mod ssr_tests {
|
||||
html! { <div class="abc"></div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, r#"<div class="abc"></div>"#);
|
||||
}
|
||||
@ -511,9 +531,10 @@ mod ssr_tests {
|
||||
html! { <div>{"Hello!"}</div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, r#"<div>Hello!</div>"#);
|
||||
}
|
||||
@ -525,9 +546,10 @@ mod ssr_tests {
|
||||
html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
|
||||
}
|
||||
@ -539,9 +561,10 @@ mod ssr_tests {
|
||||
html! { <textarea value="teststring" /> }
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
|
||||
}
|
||||
|
||||
@ -34,9 +34,15 @@ impl PartialEq for VText {
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use crate::html::AnyScope;
|
||||
|
||||
impl VText {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String) {
|
||||
pub(crate) async fn render_to_string(
|
||||
&self,
|
||||
w: &mut String,
|
||||
_parent_scope: &AnyScope,
|
||||
_hydratable: bool,
|
||||
) {
|
||||
html_escape::encode_text_to_string(&self.text, w);
|
||||
}
|
||||
}
|
||||
@ -46,16 +52,21 @@ mod feat_ssr {
|
||||
mod ssr_tests {
|
||||
use tokio::test;
|
||||
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
use crate::ServerRenderer;
|
||||
|
||||
#[test]
|
||||
async fn test_simple_str() {
|
||||
let vtext = VText::new("abc");
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
html! { "abc" }
|
||||
}
|
||||
|
||||
let mut s = String::new();
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
vtext.render_to_string(&mut s).await;
|
||||
|
||||
assert_eq!("abc", s.as_str());
|
||||
assert_eq!(s, r#"abc"#);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user