diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index dbb88e227..c2ac245af 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -31,6 +31,8 @@ pub mod prelude { pub use crate::adapters::warp_adapter::*; #[cfg(not(feature = "ssr"))] pub use crate::props_html::deserialize_props::get_root_props_from_document; + #[cfg(all(feature = "ssr", feature = "router"))] + pub use crate::render::pre_cache_static_routes_with_props; #[cfg(feature = "ssr")] pub use crate::render::SSRState; #[cfg(feature = "ssr")] diff --git a/packages/fullstack/src/props_html/mod.rs b/packages/fullstack/src/props_html/mod.rs index 048e2aa90..d2ca82f56 100644 --- a/packages/fullstack/src/props_html/mod.rs +++ b/packages/fullstack/src/props_html/mod.rs @@ -37,7 +37,8 @@ fn serialized_and_deserializes() { }; y ]; - serialize_props::serde_to_writable(&data, &mut as_string).unwrap(); + serialize_props::serde_to_writable(&data, &mut unsafe { as_string.as_bytes_mut() }) + .unwrap(); println!("{}", as_string); println!( diff --git a/packages/fullstack/src/props_html/serialize_props.rs b/packages/fullstack/src/props_html/serialize_props.rs index 0012afb9c..58bd2841d 100644 --- a/packages/fullstack/src/props_html/serialize_props.rs +++ b/packages/fullstack/src/props_html/serialize_props.rs @@ -6,8 +6,8 @@ use base64::Engine; #[allow(unused)] pub(crate) fn serde_to_writable( value: &T, - mut write_to: impl std::fmt::Write, -) -> std::fmt::Result { + write_to: &mut impl std::io::Write, +) -> std::io::Result<()> { let serialized = postcard::to_allocvec(value).unwrap(); let compressed = yazi::compress( &serialized, @@ -15,7 +15,7 @@ pub(crate) fn serde_to_writable( yazi::CompressionLevel::BestSize, ) .unwrap(); - write_to.write_str(&STANDARD.encode(compressed)); + write_to.write_all(&STANDARD.encode(compressed).as_bytes())?; Ok(()) } @@ -23,9 +23,10 @@ pub(crate) fn serde_to_writable( /// Encode data into a element. This is inteded to be used in the server to send data to the client. pub(crate) fn encode_in_element( data: T, - mut write_to: impl std::fmt::Write, -) -> std::fmt::Result { - write_to.write_str(r#""#) + write_to: &mut impl std::io::Write, +) -> std::io::Result<()> { + write_to + .write_all(r#""#.as_bytes()) } diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index 97ca4e3cf..d33870943 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -1,29 +1,24 @@ //! A shared pool of renderers for efficient server side rendering. -use std::sync::Arc; +use std::{fmt::Write, sync::Arc}; use dioxus::prelude::VirtualDom; use dioxus_ssr::{ - incremental::{IncrementalRendererConfig, RenderFreshness}, + incremental::{IncrementalRendererConfig, RenderFreshness, WrapBody}, Renderer, }; +use serde::Serialize; use crate::prelude::*; use dioxus::prelude::*; enum SsrRendererPool { Renderer(object_pool::Pool), - Incremental( - object_pool::Pool< - dioxus_ssr::incremental::IncrementalRenderer< - crate::serve_config::EmptyIncrementalRenderTemplate, - >, - >, - ), + Incremental(object_pool::Pool), } impl SsrRendererPool { - async fn render_to( + async fn render_to( &self, cfg: &ServeConfig

, route: String, @@ -32,21 +27,28 @@ impl SsrRendererPool { to: &mut String, modify_vdom: impl FnOnce(&mut VirtualDom), ) -> Result { + let wrapper = FullstackRenderer { cfg }; match self { Self::Renderer(pool) => { let mut vdom = VirtualDom::new_with_props(component, props); modify_vdom(&mut vdom); let _ = vdom.rebuild(); + let mut renderer = pool.pull(pre_renderer); + + // SAFETY: The fullstack renderer will only write UTF-8 to the buffer. + wrapper.render_before_body(unsafe { &mut to.as_bytes_mut() })?; renderer.render_to(to, &vdom)?; + wrapper.render_after_body(unsafe { &mut to.as_bytes_mut() })?; Ok(RenderFreshness::now(None)) } Self::Incremental(pool) => { - let mut renderer = pool.pull(|| incremental_pre_renderer(cfg)); + let mut renderer = + pool.pull(|| incremental_pre_renderer(cfg.incremental.as_ref().unwrap())); Ok(renderer - .render_to_string(route, component, props, to, modify_vdom) + .render_to_string(route, component, props, to, modify_vdom, &wrapper) .await?) } } @@ -66,7 +68,7 @@ impl SSRState { return Self { renderers: Arc::new(SsrRendererPool::Incremental(object_pool::Pool::new( 10, - || incremental_pre_renderer(cfg), + || incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), ))), }; } @@ -90,26 +92,48 @@ impl SSRState { > + Send + 'a { async move { - let ServeConfig { app, props, .. } = cfg; - - let ServeConfig { index, .. } = cfg; - let mut html = String::new(); - - html += &index.pre_main; + let ServeConfig { app, props, .. } = cfg; let freshness = self .renderers .render_to(cfg, route, *app, props.clone(), &mut html, modify_vdom) .await?; - // serialize the props - let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html); + Ok(RenderResponse { html, freshness }) + } + } +} - #[cfg(all(debug_assertions, feature = "hot-reload"))] - { - // In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work - let disconnect_js = r#"(function () { +struct FullstackRenderer<'a, P: Clone + Send + Sync + 'static> { + cfg: &'a ServeConfig

, +} + +impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::WrapBody + for FullstackRenderer<'a, P> +{ + fn render_before_body( + &self, + to: &mut R, + ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { + let ServeConfig { index, .. } = &self.cfg; + + to.write_all(index.pre_main.as_bytes())?; + + Ok(()) + } + + fn render_after_body( + &self, + to: &mut R, + ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { + // serialize the props + crate::props_html::serialize_props::encode_in_element(&self.cfg.props, to)?; + + #[cfg(all(debug_assertions, feature = "hot-reload"))] + { + // In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work + let disconnect_js = r#"(function () { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = protocol + '//' + window.location.host + '/_dioxus/disconnect'; const poll_interval = 1000; @@ -135,15 +159,16 @@ impl SSRState { ws.onclose = reload_upon_connect; })()"#; - html += r#""#; - } - - html += &index.post_main; - - Ok(RenderResponse { html, freshness }) + to.write_all(r#""#.as_bytes())?; } + + let ServeConfig { index, .. } = &self.cfg; + + to.write_all(index.post_main.as_bytes())?; + + Ok(()) } } @@ -171,12 +196,29 @@ fn pre_renderer() -> Renderer { renderer.into() } -fn incremental_pre_renderer( - cfg: &ServeConfig

, -) -> dioxus_ssr::incremental::IncrementalRenderer -{ - let builder: &IncrementalRendererConfig<_> = &*cfg.incremental.as_ref().unwrap(); - let mut renderer = builder.clone().build(); +fn incremental_pre_renderer( + cfg: &IncrementalRendererConfig, +) -> dioxus_ssr::incremental::IncrementalRenderer { + let mut renderer = cfg.clone().build(); renderer.renderer_mut().pre_render = true; renderer } + +#[cfg(all(feature = "ssr", feature = "router"))] +/// Pre-caches all static routes +pub async fn pre_cache_static_routes_with_props( + cfg: &crate::prelude::ServeConfig>, +) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> +where + Rt: dioxus_router::prelude::Routable + Send + Sync + Serialize, + ::Err: std::fmt::Display, +{ + let wrapper = FullstackRenderer { cfg }; + let mut renderer = incremental_pre_renderer( + cfg.incremental + .as_ref() + .expect("incremental renderer config must be set to pre-cache static routes"), + ); + + dioxus_router::incremental::pre_cache_static_routes::(&mut renderer, &wrapper).await +} diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index 3307cf4c1..75a473c89 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -17,18 +17,15 @@ pub struct ServeConfigBuilder { pub(crate) root_id: Option<&'static str>, pub(crate) index_path: Option<&'static str>, pub(crate) assets_path: Option<&'static str>, - pub(crate) incremental: Option< - std::sync::Arc< - dioxus_ssr::incremental::IncrementalRendererConfig, - >, - >, + pub(crate) incremental: + Option>, } /// A template for incremental rendering that does nothing. #[derive(Default, Clone)] pub struct EmptyIncrementalRenderTemplate; -impl dioxus_ssr::incremental::RenderHTML for EmptyIncrementalRenderTemplate { +impl dioxus_ssr::incremental::WrapBody for EmptyIncrementalRenderTemplate { fn render_after_body( &self, _: &mut R, @@ -70,10 +67,7 @@ impl ServeConfigBuilder

{ } /// Enable incremental static generation - pub fn incremental( - mut self, - cfg: dioxus_ssr::incremental::IncrementalRendererConfig, - ) -> Self { + pub fn incremental(mut self, cfg: dioxus_ssr::incremental::IncrementalRendererConfig) -> Self { self.incremental = Some(std::sync::Arc::new(cfg)); self } @@ -157,11 +151,8 @@ pub struct ServeConfig { pub(crate) props: P, pub(crate) index: IndexHtml, pub(crate) assets_path: &'static str, - pub(crate) incremental: Option< - std::sync::Arc< - dioxus_ssr::incremental::IncrementalRendererConfig, - >, - >, + pub(crate) incremental: + Option>, } impl From> for ServeConfig

{ diff --git a/packages/router/src/incremental.rs b/packages/router/src/incremental.rs index bc7713579..2aa4dce90 100644 --- a/packages/router/src/incremental.rs +++ b/packages/router/src/incremental.rs @@ -3,14 +3,15 @@ use std::str::FromStr; use dioxus::prelude::*; use dioxus_ssr::incremental::{ - IncrementalRenderer, IncrementalRendererError, RenderFreshness, RenderHTML, + IncrementalRenderer, IncrementalRendererError, RenderFreshness, WrapBody, }; use crate::prelude::*; /// Pre-cache all static routes. -pub async fn pre_cache_static_routes( - renderer: &mut IncrementalRenderer, +pub async fn pre_cache_static_routes( + renderer: &mut IncrementalRenderer, + wrapper: &R, ) -> Result<(), IncrementalRendererError> where Rt: Routable, @@ -41,7 +42,7 @@ where if is_static { match Rt::from_str(&full_path) { Ok(route) => { - render_route(renderer, route, &mut tokio::io::sink(), |_| {}).await?; + render_route(renderer, route, &mut tokio::io::sink(), |_| {}, wrapper).await?; } Err(e) => { log::info!("@ route: {}", full_path); @@ -55,11 +56,12 @@ where } /// Render a route to a writer. -pub async fn render_route( - renderer: &mut IncrementalRenderer, +pub async fn render_route( + renderer: &mut IncrementalRenderer, route: Rt, writer: &mut W, modify_vdom: F, + wrapper: &R, ) -> Result where Rt: Routable, @@ -87,6 +89,7 @@ where RenderPathProps { path: route }, writer, modify_vdom, + wrapper, ) .await } diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index bd88c5c95..9f42bfa86 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -15,7 +15,7 @@ use std::{ use tokio::io::{AsyncWrite, AsyncWriteExt, BufReader}; /// Something that can render a HTML page from a body. -pub trait RenderHTML { +pub trait WrapBody { /// Render the HTML before the body fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError>; /// Render the HTML after the body @@ -49,7 +49,7 @@ impl Default for DefaultRenderer { } } -impl RenderHTML for DefaultRenderer { +impl WrapBody for DefaultRenderer { fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError> { to.write_all(self.before_body.as_bytes())?; Ok(()) @@ -63,27 +63,25 @@ impl RenderHTML for DefaultRenderer { /// A configuration for the incremental renderer. #[derive(Debug, Clone)] -pub struct IncrementalRendererConfig { +pub struct IncrementalRendererConfig { static_dir: PathBuf, memory_cache_limit: usize, invalidate_after: Option, - render: R, } -impl Default for IncrementalRendererConfig { +impl Default for IncrementalRendererConfig { fn default() -> Self { - Self::new(R::default()) + Self::new() } } -impl IncrementalRendererConfig { +impl IncrementalRendererConfig { /// Create a new incremental renderer configuration. - pub fn new(render: R) -> Self { + pub fn new() -> Self { Self { static_dir: PathBuf::from("./static"), memory_cache_limit: 10000, invalidate_after: None, - render, } } @@ -106,30 +104,28 @@ impl IncrementalRendererConfig { } /// Build the incremental renderer. - pub fn build(self) -> IncrementalRenderer { + pub fn build(self) -> IncrementalRenderer { IncrementalRenderer { static_dir: self.static_dir, memory_cache: NonZeroUsize::new(self.memory_cache_limit) .map(|limit| lru::LruCache::with_hasher(limit, Default::default())), invalidate_after: self.invalidate_after, - render: self.render, ssr_renderer: crate::Renderer::new(), } } } /// An incremental renderer. -pub struct IncrementalRenderer { +pub struct IncrementalRenderer { static_dir: PathBuf, #[allow(clippy::type_complexity)] memory_cache: Option), BuildHasherDefault>>, invalidate_after: Option, ssr_renderer: crate::Renderer, - render: R, } -impl IncrementalRenderer { +impl IncrementalRenderer { /// Get the inner renderer. pub fn renderer(&self) -> &crate::Renderer { &self.ssr_renderer @@ -141,8 +137,8 @@ impl IncrementalRenderer { } /// Create a new incremental renderer builder. - pub fn builder(renderer: R) -> IncrementalRendererConfig { - IncrementalRendererConfig::new(renderer) + pub fn builder() -> IncrementalRendererConfig { + IncrementalRendererConfig::new() } /// Remove a route from the cache. @@ -168,13 +164,14 @@ impl IncrementalRenderer { self.invalidate_after.is_some() } - fn render_and_cache<'a, P: 'static>( + fn render_and_cache<'a, P: 'static, R: WrapBody + Send + Sync>( &'a mut self, route: String, comp: fn(Scope

) -> Element, props: P, output: &'a mut (impl AsyncWrite + Unpin + Send), modify_vdom: impl FnOnce(&mut VirtualDom), + renderer: &'a R, ) -> impl std::future::Future> + 'a + Send { let mut html_buffer = WriteBuffer { buffer: Vec::new() }; @@ -185,13 +182,13 @@ impl IncrementalRenderer { modify_vdom(&mut vdom); let _ = vdom.rebuild(); - result_1 = self.render.render_before_body(&mut *html_buffer); + result_1 = renderer.render_before_body(&mut *html_buffer); result2 = self.ssr_renderer.render_to(&mut html_buffer, &vdom); } async move { result_1?; result2?; - self.render.render_after_body(&mut *html_buffer)?; + renderer.render_after_body(&mut *html_buffer)?; let html_buffer = html_buffer.buffer; output.write_all(&html_buffer).await?; @@ -273,13 +270,14 @@ impl IncrementalRenderer { } /// Render a route or get it from cache. - pub async fn render( + pub async fn render( &mut self, route: String, component: fn(Scope

) -> Element, props: P, output: &mut (impl AsyncWrite + Unpin + std::marker::Send), modify_vdom: impl FnOnce(&mut VirtualDom), + renderer: &R, ) -> Result { // check if this route is cached if let Some(freshness) = self.search_cache(route.to_string(), output).await? { @@ -287,7 +285,7 @@ impl IncrementalRenderer { } else { // if not, create it let freshness = self - .render_and_cache(route, component, props, output, modify_vdom) + .render_and_cache(route, component, props, output, modify_vdom, renderer) .await?; log::trace!("cache miss"); Ok(freshness) @@ -295,18 +293,26 @@ impl IncrementalRenderer { } /// Render a route or get it from cache to a string. - pub async fn render_to_string( + pub async fn render_to_string( &mut self, route: String, component: fn(Scope

) -> Element, props: P, output: &mut String, modify_vdom: impl FnOnce(&mut VirtualDom), + renderer: &R, ) -> Result { unsafe { // SAFETY: The renderer will only write utf8 to the buffer - self.render(route, component, props, output.as_mut_vec(), modify_vdom) - .await + self.render( + route, + component, + props, + output.as_mut_vec(), + modify_vdom, + renderer, + ) + .await } }