Bladeren bron

feat: make hydration more robust

Jonathan Kelley 3 jaren geleden
bovenliggende
commit
bbb6ee1

+ 0 - 1
packages/core/src/mutations.rs

@@ -42,7 +42,6 @@ pub enum DomEdit<'bump> {
     PushRoot {
         root: u64,
     },
-    PopRoot,
 
     AppendChildren {
         many: u32,

+ 0 - 5
packages/desktop/src/index.js

@@ -234,10 +234,6 @@ class Interpreter {
     this.stack.push(node);
   }
 
-  PopRoot(_edit) {
-    this.stack.pop();
-  }
-
   AppendChildren(edit) {
     let root = this.stack[this.stack.length - (1 + edit.many)];
 
@@ -407,7 +403,6 @@ class Interpreter {
 function main() {
   let root = window.document.getElementById("main");
   window.interpreter = new Interpreter(root);
-  console.log(window.interpreter);
 
   rpc.call("initialize");
 }

+ 37 - 15
packages/ssr/src/lib.rs

@@ -70,6 +70,13 @@ pub fn render_vdom(dom: &VirtualDom) -> String {
     format!("{:}", TextRenderer::from_vdom(dom, SsrConfig::default()))
 }
 
+pub fn pre_render_vdom(dom: &VirtualDom) -> String {
+    format!(
+        "{:}",
+        TextRenderer::from_vdom(dom, SsrConfig::default().pre_render(true))
+    )
+}
+
 pub fn render_vdom_cfg(dom: &VirtualDom, cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> String {
     format!(
         "{:}",
@@ -114,7 +121,8 @@ pub struct TextRenderer<'a, 'b> {
 
 impl Display for TextRenderer<'_, '_> {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        self.html_render(self.root, f, 0)
+        let mut last_node_was_text = false;
+        self.html_render(self.root, f, 0, &mut last_node_was_text)
     }
 }
 
@@ -127,14 +135,27 @@ impl<'a> TextRenderer<'a, '_> {
         }
     }
 
-    fn html_render(&self, node: &VNode, f: &mut std::fmt::Formatter, il: u16) -> std::fmt::Result {
+    fn html_render(
+        &self,
+        node: &VNode,
+        f: &mut std::fmt::Formatter,
+        il: u16,
+        last_node_was_text: &mut bool,
+    ) -> std::fmt::Result {
         match &node {
             VNode::Text(text) => {
+                if *last_node_was_text && self.cfg.pre_render {
+                    write!(f, "<!--spacer-->")?;
+                }
+
                 if self.cfg.indent {
                     for _ in 0..il {
                         write!(f, "    ")?;
                     }
                 }
+
+                *last_node_was_text = true;
+
                 write!(f, "{}", text.text)?
             }
             VNode::Placeholder(_anchor) => {
@@ -184,17 +205,17 @@ impl<'a> TextRenderer<'a, '_> {
                     }
                 }
 
-                // we write the element's id as a data attribute
-                //
-                // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
-                // them interpreter's stack
-                if let (true, Some(id)) = (self.cfg.pre_render, node.try_mounted_id()) {
-                    write!(f, " dioxus-id=\"{}\"", id)?;
+                // // we write the element's id as a data attribute
+                // //
+                // // when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
+                // // them interpreter's stack
+                // if let (true, Some(id)) = (self.cfg.pre_render, node.try_mounted_id()) {
+                //     write!(f, " dioxus-id=\"{}\"", id)?;
 
-                    for _listener in el.listeners {
-                        // todo: write the listeners
-                    }
-                }
+                //     for _listener in el.listeners {
+                //         // todo: write the listeners
+                //     }
+                // }
 
                 match self.cfg.newline {
                     true => writeln!(f, ">")?,
@@ -204,8 +225,9 @@ impl<'a> TextRenderer<'a, '_> {
                 if let Some(inner_html) = inner_html {
                     write!(f, "{}", inner_html)?;
                 } else {
+                    let mut last_node_was_text = false;
                     for child in el.children {
-                        self.html_render(child, f, il + 1)?;
+                        self.html_render(child, f, il + 1, &mut last_node_was_text)?;
                     }
                 }
 
@@ -225,7 +247,7 @@ impl<'a> TextRenderer<'a, '_> {
             }
             VNode::Fragment(frag) => {
                 for child in frag.children {
-                    self.html_render(child, f, il + 1)?;
+                    self.html_render(child, f, il + 1, last_node_was_text)?;
                 }
             }
             VNode::Component(vcomp) => {
@@ -233,7 +255,7 @@ impl<'a> TextRenderer<'a, '_> {
 
                 if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
                     let new_node = vdom.get_scope(idx).unwrap().root_node();
-                    self.html_render(new_node, f, il + 1)?;
+                    self.html_render(new_node, f, il + 1, last_node_was_text)?;
                 } else {
                 }
             }

+ 4 - 2
packages/web/Cargo.toml

@@ -73,14 +73,16 @@ features = [
 # [lib]
 # crate-type = ["cdylib", "rlib"]
 
-# [dev-dependencies]
+[dev-dependencies]
+dioxus-core-macro = { path = "../core-macro" }
+wasm-bindgen-test = "0.3.28"
+dioxus-ssr = { path = "../ssr" }
 # im-rc = "15.0.0"
 # separator = "0.4.1"
 # uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] }
 # serde = { version = "1.0.126", features = ["derive"] }
 # reqwest = { version = "0.11", features = ["json"] }
 # dioxus-hooks = { path = "../hooks" }
-# dioxus-core-macro = { path = "../core-macro" }
 # rand = { version = "0.8.4", features = ["small_rng"] }
 
 # [dev-dependencies.getrandom]

+ 67 - 0
packages/web/examples/hydrate.rs

@@ -0,0 +1,67 @@
+use dioxus_core as dioxus;
+use dioxus_core::prelude::*;
+use dioxus_core_macro::*;
+use dioxus_html as dioxus_elements;
+use wasm_bindgen_test::wasm_bindgen_test;
+use web_sys::window;
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            h1 { "thing 1" }
+        }
+        div {
+            h2 { "thing 2"}
+        }
+        div {
+            h2 { "thing 2"}
+            "asd"
+            "asd"
+            bapp()
+        }
+        (0..10).map(|f| rsx!{
+            div {
+                "thing {f}"
+            }
+        })
+    })
+}
+
+fn bapp(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            h1 { "thing 1" }
+        }
+        div {
+            h2 { "thing 2"}
+        }
+        div {
+            h2 { "thing 2"}
+            "asd"
+            "asd"
+        }
+    })
+}
+
+fn main() {
+    console_error_panic_hook::set_once();
+    wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
+
+    let mut dom = VirtualDom::new(app);
+    let _ = dom.rebuild();
+
+    let pre = dioxus_ssr::pre_render_vdom(&dom);
+    log::debug!("{}", pre);
+
+    // set the inner content of main to the pre-rendered content
+    window()
+        .unwrap()
+        .document()
+        .unwrap()
+        .get_element_by_id("main")
+        .unwrap()
+        .set_inner_html(&pre);
+
+    // now rehydtrate
+    dioxus_web::launch_with_props(app, (), |c| c.hydrate(true));
+}

+ 19 - 41
packages/web/src/dom.rs

@@ -22,11 +22,11 @@ pub struct WebsysDom {
     stack: Stack,
 
     /// A map from ElementID (index) to Node
-    nodes: NodeSlab,
+    pub(crate) nodes: NodeSlab,
 
     document: Document,
 
-    root: Element,
+    pub(crate) root: Element,
 
     sender_callback: Rc<dyn Fn(SchedulerMsg)>,
 
@@ -34,45 +34,20 @@ pub struct WebsysDom {
     // This is roughly a delegater
     // TODO: check how infero delegates its events - some are more performant
     listeners: FxHashMap<&'static str, ListenerEntry>,
-
-    // We need to make sure to add comments between text nodes
-    // We ensure that the text siblings are patched by preventing the browser from merging
-    // neighboring text nodes. Originally inspired by some of React's work from 2016.
-    //  -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
-    //  -> https://github.com/facebook/react/pull/5753
-    last_node_was_text: bool,
 }
 
 type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
 
 impl WebsysDom {
-    pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
+    pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
         let document = load_document();
 
         let nodes = NodeSlab::new(2000);
         let listeners = FxHashMap::default();
 
-        // re-hydrate the page - only supports one virtualdom per page
-        // hydration is the dubmest thing you've ever heard of
-        // just blast away the page and replace it completely.
-        if cfg.hydrate {
-            // // Load all the elements into the arena
-            // let node_list: NodeList = document.query_selector_all("dioxus-id").unwrap();
-            // let len = node_list.length() as usize;
-
-            // for x in 0..len {
-            //     let node: Node = node_list.get(x as u32).unwrap();
-            //     let el: &Element = node.dyn_ref::<Element>().unwrap();
-            //     let id: String = el.get_attribute("dioxus-id").unwrap();
-            //     let id = id.parse::<usize>().unwrap();
-            //     nodes[id] = Some(node);
-            // }
-
-            // Load all the event listeners into our listener register
-            // TODO
-        }
-
         let mut stack = Stack::with_capacity(10);
+
+        let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
         let root_node = root.clone().dyn_into::<Node>().unwrap();
         stack.push(root_node);
 
@@ -83,15 +58,13 @@ impl WebsysDom {
             document,
             sender_callback,
             root,
-            last_node_was_text: false,
         }
     }
 
-    pub fn process_edits(&mut self, edits: &mut Vec<DomEdit>) {
+    pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
         for edit in edits.drain(..) {
             match edit {
                 DomEdit::PushRoot { root } => self.push(root),
-                DomEdit::PopRoot => self.pop(),
                 DomEdit::AppendChildren { many } => self.append_children(many),
                 DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
                 DomEdit::Remove { root } => self.remove(root),
@@ -137,11 +110,6 @@ impl WebsysDom {
         self.stack.push(real_node);
     }
 
-    // drop the node off the stack
-    fn pop(&mut self) {
-        self.stack.pop();
-    }
-
     fn append_children(&mut self, many: u32) {
         let root: Node = self
             .stack
@@ -150,13 +118,23 @@ impl WebsysDom {
             .unwrap()
             .clone();
 
+        // We need to make sure to add comments between text nodes
+        // We ensure that the text siblings are patched by preventing the browser from merging
+        // neighboring text nodes. Originally inspired by some of React's work from 2016.
+        //  -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
+        //  -> https://github.com/facebook/react/pull/5753
+        /*
+        todo: we need to track this for replacing/insert after/etc
+        */
+        let mut last_node_was_text = false;
+
         for child in self
             .stack
             .list
             .drain((self.stack.list.len() - many as usize)..)
         {
             if child.dyn_ref::<web_sys::Text>().is_some() {
-                if self.last_node_was_text {
+                if last_node_was_text {
                     let comment_node = self
                         .document
                         .create_comment("dioxus")
@@ -164,9 +142,9 @@ impl WebsysDom {
                         .unwrap();
                     root.append_child(&comment_node).unwrap();
                 }
-                self.last_node_was_text = true;
+                last_node_was_text = true;
             } else {
-                self.last_node_was_text = false;
+                last_node_was_text = false;
             }
             root.append_child(&child).unwrap();
         }

+ 28 - 13
packages/web/src/lib.rs

@@ -55,7 +55,6 @@
 use std::rc::Rc;
 
 pub use crate::cfg::WebConfig;
-use crate::dom::load_document;
 use dioxus::SchedulerMsg;
 use dioxus::VirtualDom;
 pub use dioxus_core as dioxus;
@@ -66,6 +65,7 @@ mod cache;
 mod cfg;
 mod dom;
 mod nodeslab;
+mod rehydrate;
 mod ric_raf;
 
 /// Launch the VirtualDOM given a root component and a configuration.
@@ -146,24 +146,39 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
         wasm_bindgen::intern(s);
     }
 
-    let should_hydrate = cfg.hydrate;
-
-    let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap();
-
     let tasks = dom.get_scheduler_channel();
 
     let sender_callback: Rc<dyn Fn(SchedulerMsg)> =
         Rc::new(move |event| tasks.unbounded_send(event).unwrap());
 
-    let mut websys_dom = dom::WebsysDom::new(root_el, cfg, sender_callback);
+    let should_hydrate = cfg.hydrate;
+
+    let mut websys_dom = dom::WebsysDom::new(cfg, sender_callback);
 
     log::trace!("rebuilding app");
-    let mut mutations = dom.rebuild();
 
-    // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding
-    // ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
-    if !should_hydrate {
-        websys_dom.process_edits(&mut mutations.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();
+
+        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.edits);
     }
 
     let work_loop = ric_raf::RafLoop::new();
@@ -185,9 +200,9 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
         // wait for the animation frame to fire so we can apply our changes
         work_loop.wait_for_raf().await;
 
-        for mut edit in mutations {
+        for edit in mutations {
             // actually apply our changes during the animation frame
-            websys_dom.process_edits(&mut edit.edits);
+            websys_dom.apply_edits(edit.edits);
         }
     }
 }

+ 157 - 0
packages/web/src/rehydrate.rs

@@ -0,0 +1,157 @@
+use crate::dom::WebsysDom;
+use dioxus_core::{VNode, VirtualDom};
+use wasm_bindgen::JsCast;
+use web_sys::{Comment, Element, Node, Text};
+
+#[derive(Debug)]
+pub enum RehydrationError {
+    NodeTypeMismatch,
+    NodeNotFound,
+    VNodeNotInitialized,
+}
+use RehydrationError::*;
+
+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
+            .root
+            .clone()
+            .dyn_into::<Node>()
+            .map_err(|_| NodeTypeMismatch)?;
+
+        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 last_node_was_text = false;
+
+        // Recursively rehydrate the dom from the VirtualDom
+        self.rehydrate_single(
+            &mut nodes,
+            &mut counter,
+            dom,
+            root_node,
+            &mut last_node_was_text,
+        )
+    }
+
+    fn rehydrate_single(
+        &mut self,
+        nodes: &mut Vec<Node>,
+        place: &mut Vec<u32>,
+        dom: &VirtualDom,
+        node: &VNode,
+        last_node_was_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::<Comment>().unwrap();
+                        assert_eq!(node_text.data(), "spacer");
+                    }
+                    *cur_place += 1;
+                }
+
+                let node = nodes
+                    .last()
+                    .unwrap()
+                    .child_nodes()
+                    .get(*cur_place)
+                    .ok_or(NodeNotFound)?;
+
+                let _text_el = node.dyn_ref::<Text>().ok_or(NodeTypeMismatch)?;
+
+                // in debug we make sure the text is the same
+                if cfg!(debug_assertions) {
+                    let contents = _text_el.node_value().unwrap();
+                    assert_eq!(t.text, contents);
+                }
+
+                *last_node_was_text = true;
+
+                self.nodes[node_id.0] = Some(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();
+
+                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::<Element>()
+                    .unwrap()
+                    .set_attribute("dioxus-id", s.as_str())
+                    .unwrap();
+
+                self.nodes[node_id.0] = Some(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)?;
+                }
+
+                place.pop();
+                nodes.pop();
+
+                if cfg!(debug_assertions) {
+                    let el = node.dyn_ref::<Element>().unwrap();
+                    let name = el.tag_name().to_lowercase();
+                    assert_eq!(name, vel.tag);
+                }
+            }
+
+            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.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();
+                self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?;
+            }
+        }
+        Ok(())
+    }
+}

+ 42 - 0
packages/web/tests/hydrate.rs

@@ -0,0 +1,42 @@
+use dioxus_core as dioxus;
+use dioxus_core::prelude::*;
+use dioxus_core_macro::*;
+use dioxus_html as dioxus_elements;
+use wasm_bindgen_test::wasm_bindgen_test;
+
+wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+#[test]
+fn makes_tree() {
+    fn app(cx: Scope) -> Element {
+        cx.render(rsx! {
+            div {
+                h1 {}
+            }
+            div {
+                h2 {}
+            }
+        })
+    }
+
+    let mut dom = VirtualDom::new(app);
+    let muts = dom.rebuild();
+
+    dbg!(muts.edits);
+}
+
+#[wasm_bindgen_test]
+fn rehydrates() {
+    fn app(cx: Scope) -> Element {
+        cx.render(rsx! {
+            div {
+                h1 {}
+            }
+            div {
+                h2 {}
+            }
+        })
+    }
+
+    dioxus_web::launch(app);
+}