mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-27 06:30:20 +00:00
Merge pull request #79 from DioxusLabs/jk/better_rehydration
Feat: Improve rehydration by using the VDom directly.
This commit is contained in:
commit
34b0cb500b
9 changed files with 349 additions and 81 deletions
|
@ -42,7 +42,6 @@ pub enum DomEdit<'bump> {
|
||||||
PushRoot {
|
PushRoot {
|
||||||
root: u64,
|
root: u64,
|
||||||
},
|
},
|
||||||
PopRoot,
|
|
||||||
|
|
||||||
AppendChildren {
|
AppendChildren {
|
||||||
many: u32,
|
many: u32,
|
||||||
|
|
|
@ -234,10 +234,6 @@ class Interpreter {
|
||||||
this.stack.push(node);
|
this.stack.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
PopRoot(_edit) {
|
|
||||||
this.stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendChildren(edit) {
|
AppendChildren(edit) {
|
||||||
let root = this.stack[this.stack.length - (1 + edit.many)];
|
let root = this.stack[this.stack.length - (1 + edit.many)];
|
||||||
|
|
||||||
|
@ -407,7 +403,6 @@ class Interpreter {
|
||||||
function main() {
|
function main() {
|
||||||
let root = window.document.getElementById("main");
|
let root = window.document.getElementById("main");
|
||||||
window.interpreter = new Interpreter(root);
|
window.interpreter = new Interpreter(root);
|
||||||
console.log(window.interpreter);
|
|
||||||
|
|
||||||
rpc.call("initialize");
|
rpc.call("initialize");
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,13 @@ pub fn render_vdom(dom: &VirtualDom) -> String {
|
||||||
format!("{:}", TextRenderer::from_vdom(dom, SsrConfig::default()))
|
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 {
|
pub fn render_vdom_cfg(dom: &VirtualDom, cfg: impl FnOnce(SsrConfig) -> SsrConfig) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{:}",
|
"{:}",
|
||||||
|
@ -114,7 +121,8 @@ pub struct TextRenderer<'a, 'b> {
|
||||||
|
|
||||||
impl Display for TextRenderer<'_, '_> {
|
impl Display for TextRenderer<'_, '_> {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
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,26 +135,42 @@ 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 {
|
match &node {
|
||||||
VNode::Text(text) => {
|
VNode::Text(text) => {
|
||||||
|
if *last_node_was_text {
|
||||||
|
write!(f, "<!--spacer-->")?;
|
||||||
|
}
|
||||||
|
|
||||||
if self.cfg.indent {
|
if self.cfg.indent {
|
||||||
for _ in 0..il {
|
for _ in 0..il {
|
||||||
write!(f, " ")?;
|
write!(f, " ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*last_node_was_text = true;
|
||||||
|
|
||||||
write!(f, "{}", text.text)?
|
write!(f, "{}", text.text)?
|
||||||
}
|
}
|
||||||
VNode::Placeholder(_anchor) => {
|
VNode::Placeholder(_anchor) => {
|
||||||
//
|
*last_node_was_text = false;
|
||||||
|
|
||||||
if self.cfg.indent {
|
if self.cfg.indent {
|
||||||
for _ in 0..il {
|
for _ in 0..il {
|
||||||
write!(f, " ")?;
|
write!(f, " ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
write!(f, "<!-- -->")?;
|
write!(f, "<!--placeholder-->")?;
|
||||||
}
|
}
|
||||||
VNode::Element(el) => {
|
VNode::Element(el) => {
|
||||||
|
*last_node_was_text = false;
|
||||||
|
|
||||||
if self.cfg.indent {
|
if self.cfg.indent {
|
||||||
for _ in 0..il {
|
for _ in 0..il {
|
||||||
write!(f, " ")?;
|
write!(f, " ")?;
|
||||||
|
@ -184,18 +208,6 @@ 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)?;
|
|
||||||
|
|
||||||
for _listener in el.listeners {
|
|
||||||
// todo: write the listeners
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.cfg.newline {
|
match self.cfg.newline {
|
||||||
true => writeln!(f, ">")?,
|
true => writeln!(f, ">")?,
|
||||||
false => write!(f, ">")?,
|
false => write!(f, ">")?,
|
||||||
|
@ -204,8 +216,9 @@ impl<'a> TextRenderer<'a, '_> {
|
||||||
if let Some(inner_html) = inner_html {
|
if let Some(inner_html) = inner_html {
|
||||||
write!(f, "{}", inner_html)?;
|
write!(f, "{}", inner_html)?;
|
||||||
} else {
|
} else {
|
||||||
|
let mut last_node_was_text = false;
|
||||||
for child in el.children {
|
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 +238,7 @@ impl<'a> TextRenderer<'a, '_> {
|
||||||
}
|
}
|
||||||
VNode::Fragment(frag) => {
|
VNode::Fragment(frag) => {
|
||||||
for child in frag.children {
|
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) => {
|
VNode::Component(vcomp) => {
|
||||||
|
@ -233,7 +246,7 @@ impl<'a> TextRenderer<'a, '_> {
|
||||||
|
|
||||||
if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
|
if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
|
||||||
let new_node = vdom.get_scope(idx).unwrap().root_node();
|
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 {
|
} else {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,14 +73,16 @@ features = [
|
||||||
# [lib]
|
# [lib]
|
||||||
# crate-type = ["cdylib", "rlib"]
|
# 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"
|
# im-rc = "15.0.0"
|
||||||
# separator = "0.4.1"
|
# separator = "0.4.1"
|
||||||
# uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] }
|
# uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] }
|
||||||
# serde = { version = "1.0.126", features = ["derive"] }
|
# serde = { version = "1.0.126", features = ["derive"] }
|
||||||
# reqwest = { version = "0.11", features = ["json"] }
|
# reqwest = { version = "0.11", features = ["json"] }
|
||||||
# dioxus-hooks = { path = "../hooks" }
|
# dioxus-hooks = { path = "../hooks" }
|
||||||
# dioxus-core-macro = { path = "../core-macro" }
|
|
||||||
# rand = { version = "0.8.4", features = ["small_rng"] }
|
# rand = { version = "0.8.4", features = ["small_rng"] }
|
||||||
|
|
||||||
# [dev-dependencies.getrandom]
|
# [dev-dependencies.getrandom]
|
||||||
|
|
67
packages/web/examples/hydrate.rs
Normal file
67
packages/web/examples/hydrate.rs
Normal file
|
@ -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));
|
||||||
|
}
|
|
@ -22,11 +22,11 @@ pub struct WebsysDom {
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
|
|
||||||
/// A map from ElementID (index) to Node
|
/// A map from ElementID (index) to Node
|
||||||
nodes: NodeSlab,
|
pub(crate) nodes: NodeSlab,
|
||||||
|
|
||||||
document: Document,
|
document: Document,
|
||||||
|
|
||||||
root: Element,
|
pub(crate) root: Element,
|
||||||
|
|
||||||
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
|
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
|
||||||
|
|
||||||
|
@ -34,45 +34,20 @@ pub struct WebsysDom {
|
||||||
// This is roughly a delegater
|
// This is roughly a delegater
|
||||||
// TODO: check how infero delegates its events - some are more performant
|
// TODO: check how infero delegates its events - some are more performant
|
||||||
listeners: FxHashMap<&'static str, ListenerEntry>,
|
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)>);
|
type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
|
||||||
|
|
||||||
impl WebsysDom {
|
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 document = load_document();
|
||||||
|
|
||||||
let nodes = NodeSlab::new(2000);
|
let nodes = NodeSlab::new(2000);
|
||||||
let listeners = FxHashMap::default();
|
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 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();
|
let root_node = root.clone().dyn_into::<Node>().unwrap();
|
||||||
stack.push(root_node);
|
stack.push(root_node);
|
||||||
|
|
||||||
|
@ -83,15 +58,13 @@ impl WebsysDom {
|
||||||
document,
|
document,
|
||||||
sender_callback,
|
sender_callback,
|
||||||
root,
|
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(..) {
|
for edit in edits.drain(..) {
|
||||||
match edit {
|
match edit {
|
||||||
DomEdit::PushRoot { root } => self.push(root),
|
DomEdit::PushRoot { root } => self.push(root),
|
||||||
DomEdit::PopRoot => self.pop(),
|
|
||||||
DomEdit::AppendChildren { many } => self.append_children(many),
|
DomEdit::AppendChildren { many } => self.append_children(many),
|
||||||
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
|
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
|
||||||
DomEdit::Remove { root } => self.remove(root),
|
DomEdit::Remove { root } => self.remove(root),
|
||||||
|
@ -137,11 +110,6 @@ impl WebsysDom {
|
||||||
self.stack.push(real_node);
|
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) {
|
fn append_children(&mut self, many: u32) {
|
||||||
let root: Node = self
|
let root: Node = self
|
||||||
.stack
|
.stack
|
||||||
|
@ -150,13 +118,23 @@ impl WebsysDom {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone();
|
.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
|
for child in self
|
||||||
.stack
|
.stack
|
||||||
.list
|
.list
|
||||||
.drain((self.stack.list.len() - many as usize)..)
|
.drain((self.stack.list.len() - many as usize)..)
|
||||||
{
|
{
|
||||||
if child.dyn_ref::<web_sys::Text>().is_some() {
|
if child.dyn_ref::<web_sys::Text>().is_some() {
|
||||||
if self.last_node_was_text {
|
if last_node_was_text {
|
||||||
let comment_node = self
|
let comment_node = self
|
||||||
.document
|
.document
|
||||||
.create_comment("dioxus")
|
.create_comment("dioxus")
|
||||||
|
@ -164,9 +142,9 @@ impl WebsysDom {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
root.append_child(&comment_node).unwrap();
|
root.append_child(&comment_node).unwrap();
|
||||||
}
|
}
|
||||||
self.last_node_was_text = true;
|
last_node_was_text = true;
|
||||||
} else {
|
} else {
|
||||||
self.last_node_was_text = false;
|
last_node_was_text = false;
|
||||||
}
|
}
|
||||||
root.append_child(&child).unwrap();
|
root.append_child(&child).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,6 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
pub use crate::cfg::WebConfig;
|
pub use crate::cfg::WebConfig;
|
||||||
use crate::dom::load_document;
|
|
||||||
use dioxus::SchedulerMsg;
|
use dioxus::SchedulerMsg;
|
||||||
use dioxus::VirtualDom;
|
use dioxus::VirtualDom;
|
||||||
pub use dioxus_core as dioxus;
|
pub use dioxus_core as dioxus;
|
||||||
|
@ -66,6 +65,7 @@ mod cache;
|
||||||
mod cfg;
|
mod cfg;
|
||||||
mod dom;
|
mod dom;
|
||||||
mod nodeslab;
|
mod nodeslab;
|
||||||
|
mod rehydrate;
|
||||||
mod ric_raf;
|
mod ric_raf;
|
||||||
|
|
||||||
/// Launch the VirtualDOM given a root component and a configuration.
|
/// 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);
|
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 tasks = dom.get_scheduler_channel();
|
||||||
|
|
||||||
let sender_callback: Rc<dyn Fn(SchedulerMsg)> =
|
let sender_callback: Rc<dyn Fn(SchedulerMsg)> =
|
||||||
Rc::new(move |event| tasks.unbounded_send(event).unwrap());
|
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");
|
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
|
if should_hydrate {
|
||||||
// ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
|
// todo: we need to split rebuild and initialize into two phases
|
||||||
if !should_hydrate {
|
// it's a waste to produce edits just to get the vdom loaded
|
||||||
websys_dom.process_edits(&mut mutations.edits);
|
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();
|
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
|
// wait for the animation frame to fire so we can apply our changes
|
||||||
work_loop.wait_for_raf().await;
|
work_loop.wait_for_raf().await;
|
||||||
|
|
||||||
for mut edit in mutations {
|
for edit in mutations {
|
||||||
// actually apply our changes during the animation frame
|
// actually apply our changes during the animation frame
|
||||||
websys_dom.process_edits(&mut edit.edits);
|
websys_dom.apply_edits(edit.edits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
157
packages/web/src/rehydrate.rs
Normal file
157
packages/web/src/rehydrate.rs
Normal file
|
@ -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
packages/web/tests/hydrate.rs
Normal file
42
packages/web/tests/hydrate.rs
Normal file
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in a new issue