feat: overhaul ssr

This commit is contained in:
Jonathan Kelley committed 2022-12-06 17:41:47 -08:00
1 parent e8ae1fb83e
commit 34d9aafe0e
16 files changed
+263 -561

No files matched your search

-37
View File
@@ -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
}
}
-50
View File
@@ -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
View File
@@ -2,8 +2,45 @@
mod cache;
pub mod config;
pub mod helpers;
pub mod renderer;
pub mod template;
pub use helpers::*;
pub use template::SsrRender;
use dioxus_core::{Element, LazyNodes, Scope, VirtualDom};
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
View File
@@ -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 {
// vdom: VirtualDom,
// cfg: SsrConfig,
// }
/// Control if elements are written onto a new line
pub newline: bool,
// impl Default for SsrRenderer {
// fn default() -> Self {
// Self::new(SsrConfig::default())
// }
// }
/// Should we sanitize text nodes? (escape HTML)
pub sanitize: bool,
// impl SsrRenderer {
// pub fn new(cfg: SsrConfig) -> Self {
// Self {
// vdom: VirtualDom::new(app),
// cfg,
// }
// }
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
pub pre_render: bool,
// pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String {
// let scope = self.vdom.base_scope();
// let root = f.call(scope);
// format!(
// "{:}",
// TextRenderer {
// cfg: self.cfg.clone(),
// root: &root,
// vdom: Some(&self.vdom),
// }
// )
// }
// Currently not implemented
// Don't proceed onto new components. Instead, put the name of the component.
pub skip_components: bool,
// fn html_render(
// &self,
// 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-->")?;
// // }
/// A cache of templates that have been rendered
template_cache: HashMap<&'static str, Rc<StringCache>>,
}
// // if self.cfg.indent {
// // for _ in 0..il {
// // write!(f, " ")?;
// // }
// // }
impl Renderer {
pub fn new() -> Self {
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)?
// // }
// // VNode::Element(el) => {
// // *last_node_was_text = false;
pub fn render_to(&mut self, buf: &mut impl Write, dom: &VirtualDom) -> std::fmt::Result {
self.render_scope(buf, dom, ScopeId(0))
}
// // if self.cfg.indent {
// // for _ in 0..il {
// // write!(f, " ")?;
// // }
// // }
pub fn render_scope(
&mut self,
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 {
// // true => writeln!(f, ">")?,
// // false => write!(f, ">")?,
// // }
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) => {
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 {
// // write!(f, "{}", inner_html)?;
// // } 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)?;
// // }
// // }
// todo: escape the text
write!(buf, "{}", text.value)?;
// // if self.cfg.newline {
// // writeln!(f)?;
// // }
// // if self.cfg.indent {
// // for _ in 0..il {
// // write!(f, " ")?;
// // }
// // }
if self.pre_render {
write!(buf, "<!--#-->")?;
}
}
DynamicNode::Fragment(nodes) => {
for child in *nodes {
self.render_template(buf, dom, child)?;
}
}
// // write!(f, "</{}>", el.tag)?;
// // if self.cfg.newline {
// // writeln!(f)?;
// // }
// // }
// // 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();
DynamicNode::Placeholder(_el) => {
if self.pre_render {
write!(buf, "<pre><pre/>")?;
}
}
},
// // 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, 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(())
// }
// }
Segment::PreRendered(contents) => write!(buf, "{}", contents)?,
}
}
// impl<'a: 'c, 'c> Display for SsrRenderer<'a, '_, 'c> {
// 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)
// }
// }
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();
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>"
);
}
-139
View File
@@ -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>"
// );
}