feat: overhaul ssr
This commit is contained in:
1 parent
e8ae1fb83e
commit
34d9aafe0e
16 files changed
+263
-561
No files matched your search
@@ -2,13 +2,13 @@
|
|||||||
//! It also proves that lifetimes work properly, especially when used with use_ref
|
//! It also proves that lifetimes work properly, especially when used with use_ref
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_ssr::config::SsrConfig;
|
use dioxus_ssr::config::Config;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut vdom = VirtualDom::new(example);
|
let mut vdom = VirtualDom::new(example);
|
||||||
_ = vdom.rebuild();
|
_ = vdom.rebuild();
|
||||||
|
|
||||||
let out = dioxus_ssr::render_vdom_cfg(&vdom, SsrConfig::default().newline(true).indent(true));
|
let out = dioxus_ssr::render_vdom_cfg(&vdom, Config::default().newline(true).indent(true));
|
||||||
println!("{}", out);
|
println!("{}", out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_ssr::config::SsrConfig;
|
use dioxus_ssr::config::Config;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// We can render VirtualDoms
|
// We can render VirtualDoms
|
||||||
@@ -26,7 +26,7 @@ fn main() {
|
|||||||
// We can configure the SSR rendering to add ids for rehydration
|
// We can configure the SSR rendering to add ids for rehydration
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
dioxus_ssr::render_vdom_cfg(&vdom, SsrConfig::default().pre_render(true))
|
dioxus_ssr::render_vdom_cfg(&vdom, Config::default().pre_render(true))
|
||||||
);
|
);
|
||||||
|
|
||||||
// We can even render as a writer
|
// We can even render as a writer
|
||||||
|
|||||||
@@ -307,7 +307,8 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
let unbounded_props = unsafe { std::mem::transmute(props) };
|
let unbounded_props = unsafe { std::mem::transmute(props) };
|
||||||
|
|
||||||
let scope = self.new_scope(unbounded_props).id;
|
let scope = self.new_scope(unbounded_props, component.name);
|
||||||
|
let scope = scope.id;
|
||||||
component.scope.set(Some(scope));
|
component.scope.set(Some(scope));
|
||||||
|
|
||||||
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub(super) fn new_scope(&mut self, props: Box<dyn AnyProps<'static>>) -> &ScopeState {
|
pub(super) fn new_scope(
|
||||||
|
&mut self,
|
||||||
|
props: Box<dyn AnyProps<'static>>,
|
||||||
|
name: &'static str,
|
||||||
|
) -> &ScopeState {
|
||||||
let parent = self.acquire_current_scope_raw();
|
let parent = self.acquire_current_scope_raw();
|
||||||
let entry = self.scopes.vacant_entry();
|
let entry = self.scopes.vacant_entry();
|
||||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||||
@@ -28,6 +32,7 @@ impl VirtualDom {
|
|||||||
id,
|
id,
|
||||||
height,
|
height,
|
||||||
props: Some(props),
|
props: Some(props),
|
||||||
|
name,
|
||||||
placeholder: Default::default(),
|
placeholder: Default::default(),
|
||||||
node_arena_1: BumpFrame::new(50),
|
node_arena_1: BumpFrame::new(50),
|
||||||
node_arena_2: BumpFrame::new(50),
|
node_arena_2: BumpFrame::new(50),
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ pub struct ScopeId(pub usize);
|
|||||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||||
pub struct ScopeState {
|
pub struct ScopeState {
|
||||||
pub(crate) render_cnt: Cell<usize>,
|
pub(crate) render_cnt: Cell<usize>,
|
||||||
|
pub(crate) name: &'static str,
|
||||||
|
|
||||||
pub(crate) node_arena_1: BumpFrame,
|
pub(crate) node_arena_1: BumpFrame,
|
||||||
pub(crate) node_arena_2: BumpFrame,
|
pub(crate) node_arena_2: BumpFrame,
|
||||||
@@ -115,6 +116,11 @@ impl<'src> ScopeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the name of this component
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
self.name
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the current render since the inception of this component
|
/// Get the current render since the inception of this component
|
||||||
///
|
///
|
||||||
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
||||||
|
|||||||
@@ -239,11 +239,10 @@ impl VirtualDom {
|
|||||||
mutations: Mutations::default(),
|
mutations: Mutations::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = dom.new_scope(Box::new(VProps::new(
|
let root = dom.new_scope(
|
||||||
root,
|
Box::new(VProps::new(root, |_, _| unreachable!(), root_props)),
|
||||||
|_, _| unreachable!(),
|
"app",
|
||||||
root_props,
|
);
|
||||||
)));
|
|
||||||
|
|
||||||
// The root component is always a suspense boundary for any async children
|
// The root component is always a suspense boundary for any async children
|
||||||
// This could be unexpected, so we might rethink this behavior later
|
// This could be unexpected, so we might rethink this behavior later
|
||||||
|
|||||||
@@ -29,17 +29,11 @@ fn state_shares() {
|
|||||||
|
|
||||||
dom.mark_dirty(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
_ = dom.render_immediate();
|
_ = dom.render_immediate();
|
||||||
assert_eq!(
|
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 1);
|
||||||
dom.base_scope().consume_context::<i32>().unwrap().as_ref(),
|
|
||||||
&1
|
|
||||||
);
|
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId(0));
|
dom.mark_dirty(ScopeId(0));
|
||||||
_ = dom.render_immediate();
|
_ = dom.render_immediate();
|
||||||
assert_eq!(
|
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 2);
|
||||||
dom.base_scope().consume_context::<i32>().unwrap().as_ref(),
|
|
||||||
&2
|
|
||||||
);
|
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId(2));
|
dom.mark_dirty(ScopeId(2));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
//! Tests related to safety of the library.
|
//! Tests related to safety of the library.
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_core::SuspenseContext;
|
use dioxus_core::SuspenseContext;
|
||||||
|
|
||||||
@@ -18,5 +20,5 @@ fn root_node_isnt_null() {
|
|||||||
|
|
||||||
// There should be a default suspense context
|
// There should be a default suspense context
|
||||||
// todo: there should also be a default error boundary
|
// todo: there should also be a default error boundary
|
||||||
assert!(scope.has_context::<SuspenseContext>().is_some());
|
assert!(scope.has_context::<Rc<SuspenseContext>>().is_some());
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ use dioxus::core::ElementId;
|
|||||||
use dioxus::core::{Mutation::*, SuspenseContext};
|
use dioxus::core::{Mutation::*, SuspenseContext};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use std::future::IntoFuture;
|
use std::future::IntoFuture;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -51,11 +52,11 @@ fn app(cx: Scope) -> Element {
|
|||||||
|
|
||||||
fn suspense_boundary(cx: Scope) -> Element {
|
fn suspense_boundary(cx: Scope) -> Element {
|
||||||
cx.use_hook(|| {
|
cx.use_hook(|| {
|
||||||
cx.provide_context(SuspenseContext::new(cx.scope_id()));
|
cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id())));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure the right types are found
|
// Ensure the right types are found
|
||||||
cx.has_context::<SuspenseContext>().unwrap();
|
cx.has_context::<Rc<SuspenseContext>>().unwrap();
|
||||||
|
|
||||||
cx.render(rsx!(async_child {}))
|
cx.render(rsx!(async_child {}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1 @@
|
|||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct SsrConfig {
|
|
||||||
/// currently not supported - control if we indent the HTML output
|
|
||||||
indent: bool,
|
|
||||||
|
|
||||||
/// Control if elements are written onto a new line
|
|
||||||
newline: bool,
|
|
||||||
|
|
||||||
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
|
|
||||||
pre_render: bool,
|
|
||||||
|
|
||||||
// Currently not implemented
|
|
||||||
// Don't proceed onto new components. Instead, put the name of the component.
|
|
||||||
// TODO: components don't have names :(
|
|
||||||
skip_components: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SsrConfig {
|
|
||||||
pub fn indent(mut self, a: bool) -> Self {
|
|
||||||
self.indent = a;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn newline(mut self, a: bool) -> Self {
|
|
||||||
self.newline = a;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pre_render(mut self, a: bool) -> Self {
|
|
||||||
self.pre_render = a;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn skip_components(mut self, a: bool) -> Self {
|
|
||||||
self.skip_components = a;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use dioxus_core::{LazyNodes, ScopeId, VirtualDom};
|
|
||||||
|
|
||||||
use crate::config::SsrConfig;
|
|
||||||
|
|
||||||
pub fn pre_render(dom: &VirtualDom) -> String {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pre_render_to(dom: &VirtualDom, write: impl Write) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_vdom(dom: &VirtualDom) -> String {
|
|
||||||
todo!()
|
|
||||||
// format!("{:}", TextRenderer::from_vdom(dom, SsrConfig::default()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pre_render_vdom(dom: &VirtualDom) -> String {
|
|
||||||
todo!()
|
|
||||||
// format!(
|
|
||||||
// "{:}",
|
|
||||||
// TextRenderer::from_vdom(dom, SsrConfig::default().pre_render(true))
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_vdom_cfg(dom: &VirtualDom, cfg: SsrConfig) -> String {
|
|
||||||
todo!()
|
|
||||||
// format!(
|
|
||||||
// "{:}",
|
|
||||||
// TextRenderer::from_vdom(dom, cfg(SsrConfig::default()))
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
|
|
||||||
todo!()
|
|
||||||
// Some(format!(
|
|
||||||
// "{:}",
|
|
||||||
// TextRenderer {
|
|
||||||
// cfg: SsrConfig::default(),
|
|
||||||
// root: vdom.get_scope(scope).unwrap().root_node(),
|
|
||||||
// vdom: Some(vdom),
|
|
||||||
// }
|
|
||||||
// ))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_lazy<'a, 'b>(f: LazyNodes<'a, 'b>) -> String {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
+40
-3
@@ -2,8 +2,45 @@
|
|||||||
|
|
||||||
mod cache;
|
mod cache;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod helpers;
|
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
pub use helpers::*;
|
use dioxus_core::{Element, LazyNodes, Scope, VirtualDom};
|
||||||
pub use template::SsrRender;
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use crate::renderer::Renderer;
|
||||||
|
|
||||||
|
/// A convenience function to render an `rsx!` call to a string
|
||||||
|
///
|
||||||
|
/// For advanced rendering, create a new `SsrRender`.
|
||||||
|
pub fn render_lazy<'a, 'b>(f: LazyNodes<'a, 'b>) -> String {
|
||||||
|
// We need to somehow get the lazy call into the virtualdom even with the lifetime
|
||||||
|
// Since the lazy lifetime is valid for this function, we can just transmute it to static temporarily
|
||||||
|
// This is okay since we're returning an owned value
|
||||||
|
struct RootProps<'a, 'b> {
|
||||||
|
caller: Cell<Option<LazyNodes<'a, 'b>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> {
|
||||||
|
let lazy = cx.props.caller.take().unwrap();
|
||||||
|
let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) };
|
||||||
|
Ok(lazy.call(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
let props: RootProps = unsafe {
|
||||||
|
std::mem::transmute(RootProps {
|
||||||
|
caller: Cell::new(Some(f)),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new_with_props(lazy_app, props);
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
Renderer::new().render(&dom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience function to render an existing VirtualDom to a string
|
||||||
|
///
|
||||||
|
/// We generally recommend creating a new `Renderer` to take advantage of template caching.
|
||||||
|
pub fn render(dom: &VirtualDom) -> String {
|
||||||
|
Renderer::new().render(dom)
|
||||||
|
}
|
||||||
+163
-128
@@ -1,146 +1,181 @@
|
|||||||
// use dioxus_core::VirtualDom;
|
use super::cache::Segment;
|
||||||
|
use crate::cache::StringCache;
|
||||||
|
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
// use crate::config::SsrConfig;
|
/// A virtualdom renderer that caches the templates it has seen for faster rendering
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Renderer {
|
||||||
|
/// should we do our best to prettify the output?
|
||||||
|
pub pretty: bool,
|
||||||
|
|
||||||
// pub struct SsrRenderer {
|
/// Control if elements are written onto a new line
|
||||||
// vdom: VirtualDom,
|
pub newline: bool,
|
||||||
// cfg: SsrConfig,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Default for SsrRenderer {
|
/// Should we sanitize text nodes? (escape HTML)
|
||||||
// fn default() -> Self {
|
pub sanitize: bool,
|
||||||
// Self::new(SsrConfig::default())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl SsrRenderer {
|
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
|
||||||
// pub fn new(cfg: SsrConfig) -> Self {
|
pub pre_render: bool,
|
||||||
// Self {
|
|
||||||
// vdom: VirtualDom::new(app),
|
|
||||||
// cfg,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
|
// Currently not implemented
|
||||||
// let scope = self.vdom.base_scope();
|
// Don't proceed onto new components. Instead, put the name of the component.
|
||||||
// let root = f.call(scope);
|
pub skip_components: bool,
|
||||||
// format!(
|
|
||||||
// "{:}",
|
|
||||||
// TextRenderer {
|
|
||||||
// cfg: self.cfg.clone(),
|
|
||||||
// root: &root,
|
|
||||||
// vdom: Some(&self.vdom),
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn html_render(
|
/// A cache of templates that have been rendered
|
||||||
// &self,
|
template_cache: HashMap<&'static str, Rc<StringCache>>,
|
||||||
// node: &VNode,
|
}
|
||||||
// f: &mut impl Write,
|
|
||||||
// il: u16,
|
|
||||||
// last_node_was_text: &mut bool,
|
|
||||||
// ) -> std::fmt::Result {
|
|
||||||
// // match &node {
|
|
||||||
// // VNode::Text(text) => {
|
|
||||||
// // if *last_node_was_text {
|
|
||||||
// // write!(f, "<!--spacer-->")?;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // if self.cfg.indent {
|
impl Renderer {
|
||||||
// // for _ in 0..il {
|
pub fn new() -> Self {
|
||||||
// // write!(f, " ")?;
|
Self::default()
|
||||||
// // }
|
}
|
||||||
// // }
|
|
||||||
|
|
||||||
// // *last_node_was_text = true;
|
pub fn render(&mut self, dom: &VirtualDom) -> String {
|
||||||
|
let mut buf = String::new();
|
||||||
|
self.render_to(&mut buf, dom).unwrap();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
// // write!(f, "{}", text.text)?
|
pub fn render_to(&mut self, buf: &mut impl Write, dom: &VirtualDom) -> std::fmt::Result {
|
||||||
// // }
|
self.render_scope(buf, dom, ScopeId(0))
|
||||||
// // VNode::Element(el) => {
|
}
|
||||||
// // *last_node_was_text = false;
|
|
||||||
|
|
||||||
// // if self.cfg.indent {
|
pub fn render_scope(
|
||||||
// // for _ in 0..il {
|
&mut self,
|
||||||
// // write!(f, " ")?;
|
buf: &mut impl Write,
|
||||||
// // }
|
dom: &VirtualDom,
|
||||||
// // }
|
scope: ScopeId,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
// We should never ever run into async or errored nodes in SSR
|
||||||
|
// Error boundaries and suspense boundaries will convert these to sync
|
||||||
|
if let RenderReturn::Sync(Ok(node)) = dom.get_scope(scope).unwrap().root_node() {
|
||||||
|
self.render_template(buf, dom, node)?
|
||||||
|
};
|
||||||
|
|
||||||
// // write!(f, "<{}", el.tag)?;
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// // let inner_html = render_attributes(el.attributes.iter(), f)?;
|
fn render_template(
|
||||||
|
&mut self,
|
||||||
|
buf: &mut impl Write,
|
||||||
|
dom: &VirtualDom,
|
||||||
|
template: &VNode,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
let entry = self
|
||||||
|
.template_cache
|
||||||
|
.entry(template.template.name)
|
||||||
|
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
|
||||||
|
.clone();
|
||||||
|
|
||||||
// // match self.cfg.newline {
|
for segment in entry.segments.iter() {
|
||||||
// // true => writeln!(f, ">")?,
|
match segment {
|
||||||
// // false => write!(f, ">")?,
|
Segment::Attr(idx) => {
|
||||||
// // }
|
let attr = &template.dynamic_attrs[*idx];
|
||||||
|
match attr.value {
|
||||||
|
AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
|
||||||
|
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
|
||||||
|
DynamicNode::Component(node) => {
|
||||||
|
if self.skip_components {
|
||||||
|
write!(buf, "<{}><{}/>", node.name, node.name)?;
|
||||||
|
} else {
|
||||||
|
let id = node.scope.get().unwrap();
|
||||||
|
let scope = dom.get_scope(id).unwrap();
|
||||||
|
let node = scope.root_node();
|
||||||
|
match node {
|
||||||
|
RenderReturn::Sync(Ok(node)) => {
|
||||||
|
self.render_template(buf, dom, node)?
|
||||||
|
}
|
||||||
|
_ => todo!(
|
||||||
|
"generally, scopes should be sync, only if being traversed"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicNode::Text(text) => {
|
||||||
|
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
|
||||||
|
if self.pre_render {
|
||||||
|
write!(buf, "<!--#-->")?;
|
||||||
|
}
|
||||||
|
|
||||||
// // if let Some(inner_html) = inner_html {
|
// todo: escape the text
|
||||||
// // write!(f, "{}", inner_html)?;
|
write!(buf, "{}", text.value)?;
|
||||||
// // } else {
|
|
||||||
// // let mut last_node_was_text = false;
|
|
||||||
// // for child in el.children {
|
|
||||||
// // self.html_render(child, f, il + 1, &mut last_node_was_text)?;
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // if self.cfg.newline {
|
if self.pre_render {
|
||||||
// // writeln!(f)?;
|
write!(buf, "<!--#-->")?;
|
||||||
// // }
|
}
|
||||||
// // if self.cfg.indent {
|
}
|
||||||
// // for _ in 0..il {
|
DynamicNode::Fragment(nodes) => {
|
||||||
// // write!(f, " ")?;
|
for child in *nodes {
|
||||||
// // }
|
self.render_template(buf, dom, child)?;
|
||||||
// // }
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// // write!(f, "</{}>", el.tag)?;
|
DynamicNode::Placeholder(_el) => {
|
||||||
// // if self.cfg.newline {
|
if self.pre_render {
|
||||||
// // writeln!(f)?;
|
write!(buf, "<pre><pre/>")?;
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
// // VNode::Fragment(frag) => match frag.children.len() {
|
},
|
||||||
// // 0 => {
|
|
||||||
// // *last_node_was_text = false;
|
|
||||||
// // if self.cfg.indent {
|
|
||||||
// // for _ in 0..il {
|
|
||||||
// // write!(f, " ")?;
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // write!(f, "<!--placeholder-->")?;
|
|
||||||
// // }
|
|
||||||
// // _ => {
|
|
||||||
// // for child in frag.children {
|
|
||||||
// // self.html_render(child, f, il + 1, last_node_was_text)?;
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // },
|
|
||||||
// // VNode::Component(vcomp) => {
|
|
||||||
// // let idx = vcomp.scope.get().unwrap();
|
|
||||||
|
|
||||||
// // if let (Some(vdom), false) = (self.vdom, self.cfg.skip_components) {
|
Segment::PreRendered(contents) => write!(buf, "{}", contents)?,
|
||||||
// // let new_node = vdom.get_scope(idx).unwrap().root_node();
|
}
|
||||||
// // self.html_render(new_node, f, il + 1, last_node_was_text)?;
|
}
|
||||||
// // } else {
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // VNode::Template(t) => {
|
|
||||||
// // if let Some(vdom) = self.vdom {
|
|
||||||
// // todo!()
|
|
||||||
// // } else {
|
|
||||||
// // panic!("Cannot render template without vdom");
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // VNode::Placeholder(_) => {
|
|
||||||
// // todo!()
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl<'a: 'c, 'c> Display for SsrRenderer<'a, '_, 'c> {
|
Ok(())
|
||||||
// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
}
|
||||||
// let mut last_node_was_text = false;
|
}
|
||||||
// self.html_render(self.root, f, 0, &mut last_node_was_text)
|
|
||||||
// }
|
#[test]
|
||||||
// }
|
fn to_string_works() {
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let dynamic = 123;
|
||||||
|
let dyn2 = "</diiiiiiiiv>"; // todo: escape this
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
|
||||||
|
"Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
|
||||||
|
div { "nest 1" }
|
||||||
|
div {}
|
||||||
|
div { "nest 2" }
|
||||||
|
"{dyn2}"
|
||||||
|
(0..5).map(|i| rsx! { div { "finalize {i}" } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
let mut renderer = Renderer::new();
|
||||||
|
|
||||||
|
let out = renderer.render(&dom);
|
||||||
|
|
||||||
|
use Segment::*;
|
||||||
|
assert_eq!(
|
||||||
|
renderer.template_cache.iter().next().unwrap().1.segments,
|
||||||
|
vec![
|
||||||
|
PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
||||||
|
Attr(0,),
|
||||||
|
PreRendered(">Hello world 1 -->".into(),),
|
||||||
|
Node(0,),
|
||||||
|
PreRendered("<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),),
|
||||||
|
Node(1,),
|
||||||
|
Node(2,),
|
||||||
|
PreRendered("</div>".into(),),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
out,
|
||||||
|
"<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --><!--#-->123<!--/#--><-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div><!--#--></diiiiiiiiv><!--/#--><div><!--#-->finalize 0<!--/#--></div><div><!--#-->finalize 1<!--/#--></div><div><!--#-->finalize 2<!--/#--></div><div><!--#-->finalize 3<!--/#--></div><div><!--#-->finalize 4<!--/#--></div></div>"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,140 +1 @@
|
|||||||
use super::cache::Segment;
|
|
||||||
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn, VText};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::Write;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::cache::StringCache;
|
|
||||||
|
|
||||||
/// A virtualdom renderer that caches the templates it has seen for faster rendering
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct SsrRender {
|
|
||||||
template_cache: HashMap<&'static str, Rc<StringCache>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SsrRender {
|
|
||||||
pub fn render_vdom(&mut self, dom: &VirtualDom) -> String {
|
|
||||||
let scope = dom.base_scope();
|
|
||||||
let root = scope.root_node();
|
|
||||||
|
|
||||||
let mut out = String::new();
|
|
||||||
|
|
||||||
match root {
|
|
||||||
RenderReturn::Sync(Ok(node)) => self.render_template(&mut out, dom, node).unwrap(),
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_template(
|
|
||||||
&mut self,
|
|
||||||
buf: &mut String,
|
|
||||||
dom: &VirtualDom,
|
|
||||||
template: &VNode,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
let entry = self
|
|
||||||
.template_cache
|
|
||||||
.entry(template.template.name)
|
|
||||||
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
for segment in entry.segments.iter() {
|
|
||||||
match segment {
|
|
||||||
Segment::Attr(idx) => {
|
|
||||||
let attr = &template.dynamic_attrs[*idx];
|
|
||||||
match attr.value {
|
|
||||||
AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
|
|
||||||
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
|
|
||||||
DynamicNode::Component(node) => {
|
|
||||||
let id = node.scope.get().unwrap();
|
|
||||||
let scope = dom.get_scope(id).unwrap();
|
|
||||||
let node = scope.root_node();
|
|
||||||
match node {
|
|
||||||
RenderReturn::Sync(Ok(node)) => self.render_template(buf, dom, node)?,
|
|
||||||
_ => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DynamicNode::Text(text) => {
|
|
||||||
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
|
|
||||||
// if !*inner {
|
|
||||||
// write!(buf, "<!--#-->")?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// todo: escape the text
|
|
||||||
write!(buf, "{}", text.value)?;
|
|
||||||
|
|
||||||
// if !*inner {
|
|
||||||
// write!(buf, "<!--/#-->")?;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
DynamicNode::Fragment(nodes) => {
|
|
||||||
for child in *nodes {
|
|
||||||
self.render_template(buf, dom, child)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DynamicNode::Placeholder(_el) => {
|
|
||||||
// todo write a placeholder if in pre-render mode
|
|
||||||
// write!(buf, "<!--placeholder-->")?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
Segment::PreRendered(contents) => buf.push_str(contents),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_string_works() {
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
let dynamic = 123;
|
|
||||||
let dyn2 = "</diiiiiiiiv>"; // todo: escape this
|
|
||||||
|
|
||||||
render! {
|
|
||||||
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
|
|
||||||
"Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
|
|
||||||
div { "nest 1" }
|
|
||||||
div {}
|
|
||||||
div { "nest 2" }
|
|
||||||
"{dyn2}"
|
|
||||||
(0..5).map(|i| rsx! { div { "finalize {i}" } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
dom.rebuild();
|
|
||||||
|
|
||||||
use Segment::*;
|
|
||||||
|
|
||||||
// assert_eq!(
|
|
||||||
// StringCache::from_template(&dom.base_scope().root_node())
|
|
||||||
// .unwrap()
|
|
||||||
// .segments,
|
|
||||||
// vec![
|
|
||||||
// PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
|
||||||
// Attr(0,),
|
|
||||||
// PreRendered(">Hello world 1 -->".into(),),
|
|
||||||
// Node(0,),
|
|
||||||
// PreRendered("<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),),
|
|
||||||
// Node(1,),
|
|
||||||
// Node(2,),
|
|
||||||
// PreRendered("</div>".into(),),
|
|
||||||
// ]
|
|
||||||
// );
|
|
||||||
|
|
||||||
// assert_eq!(
|
|
||||||
// SsrRender::default().render_vdom(&dom),
|
|
||||||
// "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --><!--#-->123<!--/#--><-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div><!--#--></diiiiiiiiv><!--/#--><div><!--#-->finalize 0<!--/#--></div><div><!--#-->finalize 1<!--/#--></div><div><!--#-->finalize 2<!--/#--></div><div><!--#-->finalize 3<!--/#--></div><div><!--#-->finalize 4<!--/#--></div></div>"
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use dioxus_ssr::{render_lazy, render_vdom, render_vdom_cfg, SsrConfig, SsrRenderer, TextRenderer};
|
|
||||||
|
|
||||||
static SIMPLE_APP: Component = |cx| {
|
|
||||||
cx.render(rsx!(div {
|
|
||||||
"hello world!"
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
static SLIGHTLY_MORE_COMPLEX: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
div { title: "About W3Schools",
|
|
||||||
(0..20).map(|f| rsx!{
|
|
||||||
div {
|
|
||||||
title: "About W3Schools",
|
|
||||||
style: "color:blue;text-align:center",
|
|
||||||
class: "About W3Schools",
|
|
||||||
p {
|
|
||||||
title: "About W3Schools",
|
|
||||||
"Hello world!: {f}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
static NESTED_APP: Component = |cx| {
|
|
||||||
cx.render(rsx!(
|
|
||||||
div {
|
|
||||||
SIMPLE_APP {}
|
|
||||||
}
|
|
||||||
))
|
|
||||||
};
|
|
||||||
static FRAGMENT_APP: Component = |cx| {
|
|
||||||
cx.render(rsx!(
|
|
||||||
div { "f1" }
|
|
||||||
div { "f2" }
|
|
||||||
div { "f3" }
|
|
||||||
div { "f4" }
|
|
||||||
))
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_string_works() {
|
|
||||||
let mut dom = VirtualDom::new(SIMPLE_APP);
|
|
||||||
dom.rebuild();
|
|
||||||
dbg!(render_vdom(&dom));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hydration() {
|
|
||||||
let mut dom = VirtualDom::new(NESTED_APP);
|
|
||||||
dom.rebuild();
|
|
||||||
dbg!(render_vdom_cfg(&dom, |c| c.pre_render(true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn nested() {
|
|
||||||
let mut dom = VirtualDom::new(NESTED_APP);
|
|
||||||
dom.rebuild();
|
|
||||||
dbg!(render_vdom(&dom));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fragment_app() {
|
|
||||||
let mut dom = VirtualDom::new(FRAGMENT_APP);
|
|
||||||
dom.rebuild();
|
|
||||||
dbg!(render_vdom(&dom));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn write_to_file() {
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
let mut file = File::create("index.html").unwrap();
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(SLIGHTLY_MORE_COMPLEX);
|
|
||||||
dom.rebuild();
|
|
||||||
|
|
||||||
file.write_fmt(format_args!(
|
|
||||||
"{}",
|
|
||||||
TextRenderer::from_vdom(&dom, SsrConfig::default())
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn styles() {
|
|
||||||
static STLYE_APP: Component = |cx| {
|
|
||||||
cx.render(rsx! {
|
|
||||||
div { color: "blue", font_size: "46px" }
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(STLYE_APP);
|
|
||||||
dom.rebuild();
|
|
||||||
dbg!(render_vdom(&dom));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lazy() {
|
|
||||||
let p1 = SsrRenderer::new(|c| c).render_lazy(rsx! {
|
|
||||||
div { "ello" }
|
|
||||||
});
|
|
||||||
|
|
||||||
let p2 = render_lazy(rsx! {
|
|
||||||
div {
|
|
||||||
"ello"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
assert_eq!(p1, p2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn big_lazy() {
|
|
||||||
let s = render_lazy(rsx! {
|
|
||||||
div {
|
|
||||||
div {
|
|
||||||
div {
|
|
||||||
h1 { "ello world" }
|
|
||||||
h1 { "ello world" }
|
|
||||||
h1 { "ello world" }
|
|
||||||
h1 { "ello world" }
|
|
||||||
h1 { "ello world" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dbg!(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn inner_html() {
|
|
||||||
let s = render_lazy(rsx! {
|
|
||||||
div {
|
|
||||||
dangerous_inner_html: "<div> ack </div>"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dbg!(s);
|
|
||||||
}
|
|
||||||
@@ -7,66 +7,43 @@ fn simple() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
dom.rebuild();
|
_ = dom.rebuild();
|
||||||
|
|
||||||
|
assert_eq!(dioxus_ssr::render(&dom), "<div>hello!</div>");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
dioxus_ssr::render_lazy(rsx!( div {"hello!"} )),
|
||||||
"<div>hello!</div>"
|
"<div>hello!</div>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lists() {
|
fn lists() {
|
||||||
fn app(cx: Scope) -> Element {
|
assert_eq!(
|
||||||
render! {
|
dioxus_ssr::render_lazy(rsx! {
|
||||||
ul {
|
ul {
|
||||||
(0..5).map(|i| rsx! {
|
(0..5).map(|i| rsx! {
|
||||||
li { "item {i}" }
|
li { "item {i}" }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
|
||||||
"<ul><li>item 0</li><li>item 1</li><li>item 2</li><li>item 3</li><li>item 4</li></ul>"
|
"<ul><li>item 0</li><li>item 1</li><li>item 2</li><li>item 3</li><li>item 4</li></ul>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dynamic() {
|
fn dynamic() {
|
||||||
fn app(cx: Scope) -> Element {
|
let dynamic = 123;
|
||||||
let dynamic = 123;
|
|
||||||
|
|
||||||
render! {
|
|
||||||
div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
dioxus_ssr::render_lazy(rsx! {
|
||||||
|
div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" }
|
||||||
|
}),
|
||||||
"<div>Hello world 1 -->123<-- Hello world 2</div>"
|
"<div>Hello world 1 -->123<-- Hello world 2</div>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn components() {
|
fn components() {
|
||||||
fn app(cx: Scope) -> Element {
|
|
||||||
render! {
|
|
||||||
div {
|
|
||||||
(0..5).map(|name| rsx! {
|
|
||||||
my_component { name: name }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
#[inline_props]
|
||||||
fn my_component(cx: Scope, name: i32) -> Element {
|
fn my_component(cx: Scope, name: i32) -> Element {
|
||||||
render! {
|
render! {
|
||||||
@@ -74,11 +51,26 @@ fn components() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
dom.rebuild();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dioxus_ssr::SsrRender::default().render_vdom(&dom),
|
dioxus_ssr::render_lazy(rsx! {
|
||||||
|
div {
|
||||||
|
(0..5).map(|name| rsx! {
|
||||||
|
my_component { name: name }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
"<div><div>component 0</div><div>component 1</div><div>component 2</div><div>component 3</div><div>component 4</div></div>"
|
"<div><div>component 0</div><div>component 1</div><div>component 2</div><div>component 3</div><div>component 4</div></div>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fragments() {
|
||||||
|
assert_eq!(
|
||||||
|
dioxus_ssr::render_lazy(rsx! {
|
||||||
|
div {
|
||||||
|
(0..5).map(|_| rsx! (()))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
"<div></div>"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user