From 696109db10e0f76fca33de2265680c0c381b462f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 31 Jan 2023 14:10:48 -0600 Subject: [PATCH 1/3] implement hydration --- packages/core/src/lib.rs | 3 +- packages/core/src/mutations.rs | 2 +- packages/web/src/cfg.rs | 3 + packages/web/src/dom.rs | 10 +- packages/web/src/lib.rs | 62 +++--- packages/web/src/rehydrate.rs | 368 +++++++++++++++++++++------------ 6 files changed, 280 insertions(+), 168 deletions(-) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index c4f16258..304197da 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -73,7 +73,8 @@ pub use crate::innerlude::{ fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, - TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom, + TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, + VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 551069c4..49ec0ef8 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -26,7 +26,7 @@ pub struct Mutations<'a> { /// Any templates encountered while diffing the DOM. /// /// These must be loaded into a cache before applying the edits - pub templates: Vec>, + pub templates: Vec>, /// Any mutations required to patch the renderer to match the layout of the VirtualDom pub edits: Vec>, diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 1e84b1d9..7e47ed46 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -8,6 +8,7 @@ /// dioxus_web::launch(App, Config::new().hydrate(true).root_name("myroot")) /// ``` pub struct Config { + #[cfg(feature = "hydrate")] pub(crate) hydrate: bool, pub(crate) rootname: String, pub(crate) cached_strings: Vec, @@ -17,6 +18,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { + #[cfg(feature = "hydrate")] hydrate: false, rootname: "main".to_string(), cached_strings: Vec::new(), @@ -33,6 +35,7 @@ impl Config { Self::default() } + #[cfg(feature = "hydrate")] /// Enable SSR hydration /// /// This enables Dioxus to pick up work from a pre-renderd HTML file. Hydration will completely skip over any async diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 4497b5fe..4d17749d 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -22,9 +22,11 @@ use crate::Config; pub struct WebsysDom { document: Document, + #[allow(dead_code)] + pub(crate) root: Element, templates: FxHashMap, max_template_id: u32, - interpreter: Channel, + pub(crate) interpreter: Channel, } pub struct UiEvent { @@ -72,10 +74,14 @@ impl WebsysDom { } })); - dioxus_interpreter_js::initilize(root.unchecked_into(), handler.as_ref().unchecked_ref()); + dioxus_interpreter_js::initilize( + root.clone().unchecked_into(), + handler.as_ref().unchecked_ref(), + ); handler.forget(); Self { document, + root, interpreter, templates: FxHashMap::default(), max_template_id: 0, diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index db2d1c7a..ee8ed8d1 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -62,6 +62,8 @@ mod cache; mod cfg; mod dom; mod hot_reload; +#[cfg(feature = "hydrate")] +mod rehydrate; mod util; // Currently disabled since it actually slows down immediate rendering @@ -179,17 +181,40 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop wasm_bindgen::intern(s); } - let _should_hydrate = cfg.hydrate; - let (tx, mut rx) = futures_channel::mpsc::unbounded(); + #[cfg(feature = "hydrate")] + let should_hydrate = cfg.hydrate; + #[cfg(not(feature = "hydrate"))] + let should_hydrate = false; + let mut websys_dom = dom::WebsysDom::new(cfg, tx); log::info!("rebuilding app"); - // if should_hydrate { - // } else { - { + if should_hydrate { + #[cfg(feature = "hydrate")] + { + // todo: we need to split rebuild and initialize into two phases + // it's a waste to produce edits just to get the vdom loaded + + // we need to save the templates in case hydration fails + let templates = dom.rebuild().templates; + if let Err(err) = websys_dom.rehydrate(&dom) { + log::error!( + "Rehydration failed {:?}. Rebuild DOM into element from scratch", + &err + ); + websys_dom.root.set_text_content(None); + + let edits = dom.rebuild(); + + websys_dom.load_templates(&templates); + websys_dom.load_templates(&edits.templates); + websys_dom.apply_edits(edits.edits); + } + } + } else { let edits = dom.rebuild(); websys_dom.load_templates(&edits.templates); @@ -249,30 +274,3 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop websys_dom.apply_edits(edits.edits); } } - -// if should_hydrate { -// // todo: we need to split rebuild and initialize into two phases -// // it's a waste to produce edits just to get the vdom loaded -// let _ = dom.rebuild(); - -// #[cfg(feature = "hydrate")] -// #[allow(unused_variables)] -// if let Err(err) = websys_dom.rehydrate(&dom) { -// log::error!( -// "Rehydration failed {:?}. Rebuild DOM into element from scratch", -// &err -// ); - -// websys_dom.root.set_text_content(None); - -// // errrrr we should split rebuild into two phases -// // one that initializes things and one that produces edits -// let edits = dom.rebuild(); - -// websys_dom.apply_edits(edits.edits); -// } -// } else { -// let edits = dom.rebuild(); -// websys_dom.apply_edits(edits.template_mutations); -// websys_dom.apply_edits(edits.edits); -// } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index bf327176..d6afffea 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -1,9 +1,13 @@ use crate::dom::WebsysDom; -use dioxus_core::{VNode, VirtualDom}; +use dioxus_core::{ + AttributeValue, DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VPlaceholder, VText, + VirtualDom, +}; +use dioxus_html::event_bubbles; use wasm_bindgen::JsCast; -use web_sys::{Comment, Element, Node, Text}; +use web_sys::{Comment, Node}; -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub enum RehydrationError { NodeTypeMismatch, NodeNotFound, @@ -11,163 +15,263 @@ pub enum RehydrationError { } use RehydrationError::*; +fn set_node(hydrated: &mut Vec, id: ElementId, node: Node) { + let idx = id.0; + if idx >= hydrated.len() { + hydrated.resize(idx + 1, false); + } + if !hydrated[idx] { + dioxus_interpreter_js::set_node(idx as u32, node); + hydrated[idx] = true; + } +} + impl WebsysDom { // we're streaming in patches, but the nodes already exist // so we're just going to write the correct IDs to the node and load them in pub fn rehydrate(&mut self, dom: &VirtualDom) -> Result<(), RehydrationError> { - let root = self + let mut root = self .root .clone() .dyn_into::() - .map_err(|_| NodeTypeMismatch)?; + .map_err(|_| NodeTypeMismatch)? + .first_child() + .ok_or(NodeNotFound); let root_scope = dom.base_scope(); - let root_node = root_scope.root_node(); - let mut nodes = vec![root]; - let mut counter = vec![0]; + let mut hydrated = vec![true]; - let mut last_node_was_text = false; + let mut last_node_was_static_text = false; - todo!() - // // Recursively rehydrate the dom from the VirtualDom - // self.rehydrate_single( - // &mut nodes, - // &mut counter, - // dom, - // root_node, - // &mut last_node_was_text, - // ) + // Recursively rehydrate the dom from the VirtualDom + self.rehydrate_scope( + root_scope, + &mut root, + &mut hydrated, + dom, + &mut last_node_was_static_text, + )?; + + self.interpreter.flush(); + Ok(()) } - fn rehydrate_single( + fn rehydrate_scope( &mut self, - nodes: &mut Vec, - place: &mut Vec, + scope: &ScopeState, + current_child: &mut Result, + hydrated: &mut Vec, dom: &VirtualDom, - node: &VNode, - last_node_was_text: &mut bool, + last_node_was_static_text: &mut bool, + ) -> Result<(), RehydrationError> { + let vnode = match scope.root_node() { + dioxus_core::RenderReturn::Ready(ready) => ready, + _ => return Err(VNodeNotInitialized), + }; + self.rehydrate_vnode( + current_child, + hydrated, + dom, + vnode, + last_node_was_static_text, + ) + } + + fn rehydrate_vnode( + &mut self, + current_child: &mut Result, + hydrated: &mut Vec, + dom: &VirtualDom, + vnode: &VNode, + last_node_was_static_text: &mut bool, + ) -> Result<(), RehydrationError> { + for (i, root) in vnode.template.get().roots.iter().enumerate() { + // make sure we set the root node ids even if the node is not dynamic + set_node( + hydrated, + vnode.root_ids.get(i).ok_or(VNodeNotInitialized)?, + current_child.clone()?, + ); + + self.rehydrate_template_node( + current_child, + hydrated, + dom, + vnode, + root, + last_node_was_static_text, + )?; + } + Ok(()) + } + + fn rehydrate_template_node( + &mut self, + current_child: &mut Result, + hydrated: &mut Vec, + dom: &VirtualDom, + vnode: &VNode, + node: &TemplateNode, + last_node_was_static_text: &mut bool, ) -> Result<(), RehydrationError> { match node { - VNode::Text(t) => { - let node_id = t.id.get().ok_or(VNodeNotInitialized)?; - - let cur_place = place.last_mut().unwrap(); - - // skip over the comment element - if *last_node_was_text { - if cfg!(debug_assertions) { - let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - let node_text = node.dyn_into::().unwrap(); - assert_eq!(node_text.data(), "spacer"); + TemplateNode::Element { + children, attrs, .. + } => { + let mut mounted_id = None; + for attr in *attrs { + if let dioxus_core::TemplateAttribute::Dynamic { id } = attr { + let attribute = &vnode.dynamic_attrs[*id]; + let value = &attribute.value; + let id = attribute.mounted_element.get(); + mounted_id = Some(id); + let name = attribute.name; + if let AttributeValue::Listener(_) = value { + self.interpreter.new_event_listener( + &name[2..], + id.0 as u32, + event_bubbles(name) as u8, + ); + } } - *cur_place += 1; } + if let Some(id) = mounted_id { + set_node(hydrated, id, current_child.clone()?); + } + if !children.is_empty() { + let mut children_current_child = current_child + .as_mut() + .map_err(|e| *e)? + .first_child() + .ok_or(NodeNotFound)? + .dyn_into::() + .map_err(|_| NodeTypeMismatch); + for child in *children { + self.rehydrate_template_node( + &mut children_current_child, + hydrated, + dom, + vnode, + child, + last_node_was_static_text, + )?; + } + } + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + *last_node_was_static_text = false; + } + TemplateNode::Text { .. } => { + // if the last node was static text, it got merged with this one + if !*last_node_was_static_text { + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + } + *last_node_was_static_text = true; + } + TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { + self.rehydrate_dynamic_node( + current_child, + hydrated, + dom, + &vnode.dynamic_nodes[*id], + last_node_was_static_text, + )?; + } + } + Ok(()) + } - let node = nodes - .last() - .unwrap() - .child_nodes() - .get(*cur_place) - .ok_or(NodeNotFound)?; - - let _text_el = node.dyn_ref::().ok_or(NodeTypeMismatch)?; - - // in debug we make sure the text is the same + fn rehydrate_dynamic_node( + &mut self, + current_child: &mut Result, + hydrated: &mut Vec, + dom: &VirtualDom, + dynamic: &DynamicNode, + last_node_was_static_text: &mut bool, + ) -> Result<(), RehydrationError> { + match dynamic { + dioxus_core::DynamicNode::Text(VText { id, .. }) => { + // skip comment separator before node if cfg!(debug_assertions) { - let contents = _text_el.node_value().unwrap(); - assert_eq!(t.text, contents); + assert!(current_child + .as_mut() + .map_err(|e| *e)? + .has_type::()); } + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); - *last_node_was_text = true; - - self.interpreter.SetNode(node_id.0, node); - - *cur_place += 1; - } - - VNode::Element(vel) => { - let node_id = vel.id.get().ok_or(VNodeNotInitialized)?; - - let cur_place = place.last_mut().unwrap(); - - let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - - self.interpreter.SetNode(node_id.0, node.clone()); - - *cur_place += 1; - - nodes.push(node.clone()); - - place.push(0); - - // we cant have the last node be text - let mut last_node_was_text = false; - for child in vel.children { - self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; - } - - for listener in vel.listeners { - let id = listener.mounted_node.get().unwrap(); - self.interpreter.NewEventListener( - listener.event, - Some(id.as_u64()), - self.handler.as_ref().unchecked_ref(), - event_bubbles(listener.event), - ); - } - - if !vel.listeners.is_empty() { - use smallstr::SmallString; - use std::fmt::Write; - - // 8 digits is enough, yes? - // 12 million nodes in one page? - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", node_id).unwrap(); - - node.dyn_ref::() - .unwrap() - .set_attribute("dioxus-id", s.as_str()) - .unwrap(); - } - - place.pop(); - nodes.pop(); + set_node( + hydrated, + id.get().ok_or(VNodeNotInitialized)?, + current_child.clone()?, + ); + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + // skip comment separator after node if cfg!(debug_assertions) { - let el = node.dyn_ref::().unwrap(); - let name = el.tag_name().to_lowercase(); - assert_eq!(name, vel.tag); + assert!(current_child + .as_mut() + .map_err(|e| *e)? + .has_type::()); + } + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + + *last_node_was_static_text = false; + } + dioxus_core::DynamicNode::Placeholder(VPlaceholder { id, .. }) => { + set_node( + hydrated, + id.get().ok_or(VNodeNotInitialized)?, + current_child.clone()?, + ); + *current_child = current_child + .as_mut() + .map_err(|e| *e)? + .next_sibling() + .ok_or(NodeNotFound); + *last_node_was_static_text = false; + } + dioxus_core::DynamicNode::Component(comp) => { + let scope = comp.scope.get().ok_or(VNodeNotInitialized)?; + self.rehydrate_scope( + dom.get_scope(scope).unwrap(), + current_child, + hydrated, + dom, + last_node_was_static_text, + )?; + } + dioxus_core::DynamicNode::Fragment(fragment) => { + for vnode in *fragment { + self.rehydrate_vnode( + current_child, + hydrated, + dom, + vnode, + last_node_was_static_text, + )?; } } - - VNode::Placeholder(el) => { - let node_id = el.id.get().ok_or(VNodeNotInitialized)?; - - let cur_place = place.last_mut().unwrap(); - let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - - self.interpreter.SetNode(node_id.0, node); - - // self.nodes[node_id.0] = Some(node); - - *cur_place += 1; - } - - VNode::Fragment(el) => { - for el in el.children { - self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; - } - } - - VNode::Component(el) => { - let scope = dom.get_scope(el.scope.get().unwrap()).unwrap(); - let node = scope.root_node(); - todo!() - // self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?; - } - VNode::TemplateRef(_) => todo!(), } Ok(()) } From 9baef6bcd9516848db378fd3588467d73c92d083 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 31 Jan 2023 14:18:34 -0600 Subject: [PATCH 2/3] always load the templates in hydration --- packages/core/src/mutations.rs | 2 +- packages/web/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 49ec0ef8..551069c4 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -26,7 +26,7 @@ pub struct Mutations<'a> { /// Any templates encountered while diffing the DOM. /// /// These must be loaded into a cache before applying the edits - pub templates: Vec>, + pub templates: Vec>, /// Any mutations required to patch the renderer to match the layout of the VirtualDom pub edits: Vec>, diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ee8ed8d1..e208b059 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -198,8 +198,9 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // todo: we need to split rebuild and initialize into two phases // it's a waste to produce edits just to get the vdom loaded - // we need to save the templates in case hydration fails let templates = dom.rebuild().templates; + websys_dom.load_templates(&templates); + if let Err(err) = websys_dom.rehydrate(&dom) { log::error!( "Rehydration failed {:?}. Rebuild DOM into element from scratch", @@ -209,7 +210,6 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop let edits = dom.rebuild(); - websys_dom.load_templates(&templates); websys_dom.load_templates(&edits.templates); websys_dom.apply_edits(edits.edits); } From ea46db09662fb9d30b2215ef47796c8c51e61d60 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 31 Jan 2023 14:19:19 -0600 Subject: [PATCH 3/3] enable hydration feature in dev-dependancies --- packages/web/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 5ebecb60..805cacdd 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -88,4 +88,4 @@ dioxus = { path = "../dioxus", version = "0.3.0" } wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr", version = "0.3.0"} wasm-logger = "0.2.0" -dioxus-web = { path = "." } +dioxus-web = { path = ".", features = ["hydrate"] }