Selaa lähdekoodia

make fullstack helpers compatable with prerendering

Evan Almloff 2 vuotta sitten
vanhempi
commit
f5c60eeb4c

+ 2 - 0
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")]

+ 2 - 1
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!(

+ 9 - 8
packages/fullstack/src/props_html/serialize_props.rs

@@ -6,8 +6,8 @@ use base64::Engine;
 #[allow(unused)]
 pub(crate) fn serde_to_writable<T: Serialize>(
     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<T: Serialize>(
         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<T: Serialize>(
 /// 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<T: Serialize>(
     data: T,
-    mut write_to: impl std::fmt::Write,
-) -> std::fmt::Result {
-    write_to.write_str(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#)?;
-    serde_to_writable(&data, &mut write_to)?;
-    write_to.write_str(r#"" />"#)
+    write_to: &mut impl std::io::Write,
+) -> std::io::Result<()> {
+    write_to
+        .write_all(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#.as_bytes())?;
+    serde_to_writable(&data, write_to)?;
+    write_to.write_all(r#"" />"#.as_bytes())
 }

+ 80 - 38
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<Renderer>),
-    Incremental(
-        object_pool::Pool<
-            dioxus_ssr::incremental::IncrementalRenderer<
-                crate::serve_config::EmptyIncrementalRenderTemplate,
-            >,
-        >,
-    ),
+    Incremental(object_pool::Pool<dioxus_ssr::incremental::IncrementalRenderer>),
 }
 
 impl SsrRendererPool {
-    async fn render_to<P: Clone + 'static>(
+    async fn render_to<P: Clone + Serialize + Send + Sync + 'static>(
         &self,
         cfg: &ServeConfig<P>,
         route: String,
@@ -32,21 +27,28 @@ impl SsrRendererPool {
         to: &mut String,
         modify_vdom: impl FnOnce(&mut VirtualDom),
     ) -> Result<RenderFreshness, dioxus_ssr::incremental::IncrementalRendererError> {
+        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<P>,
+}
+
+impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::WrapBody
+    for FullstackRenderer<'a, P>
+{
+    fn render_before_body<R: std::io::Write>(
+        &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<R: std::io::Write>(
+        &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#"<script>"#;
-                html += disconnect_js;
-                html += r#"</script>"#;
-            }
+            to.write_all(r#"<script>"#.as_bytes())?;
+            to.write_all(disconnect_js.as_bytes())?;
+            to.write_all(r#"</script>"#.as_bytes())?;
+        }
 
-            html += &index.post_main;
+        let ServeConfig { index, .. } = &self.cfg;
 
-            Ok(RenderResponse { html, freshness })
-        }
+        to.write_all(index.post_main.as_bytes())?;
+
+        Ok(())
     }
 }
 
@@ -171,12 +196,29 @@ fn pre_renderer() -> Renderer {
     renderer.into()
 }
 
-fn incremental_pre_renderer<P: Clone>(
-    cfg: &ServeConfig<P>,
-) -> dioxus_ssr::incremental::IncrementalRenderer<crate::serve_config::EmptyIncrementalRenderTemplate>
-{
-    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<Rt>(
+    cfg: &crate::prelude::ServeConfig<crate::router::FullstackRouterConfig<Rt>>,
+) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError>
+where
+    Rt: dioxus_router::prelude::Routable + Send + Sync + Serialize,
+    <Rt as std::str::FromStr>::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::<Rt, _>(&mut renderer, &wrapper).await
+}

+ 6 - 15
packages/fullstack/src/serve_config.rs

@@ -17,18 +17,15 @@ pub struct ServeConfigBuilder<P: Clone> {
     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<EmptyIncrementalRenderTemplate>,
-        >,
-    >,
+    pub(crate) incremental:
+        Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
 }
 
 /// 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<R: std::io::Write>(
         &self,
         _: &mut R,
@@ -70,10 +67,7 @@ impl<P: Clone> ServeConfigBuilder<P> {
     }
 
     /// Enable incremental static generation
-    pub fn incremental(
-        mut self,
-        cfg: dioxus_ssr::incremental::IncrementalRendererConfig<EmptyIncrementalRenderTemplate>,
-    ) -> 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<P: Clone> {
     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<EmptyIncrementalRenderTemplate>,
-        >,
-    >,
+    pub(crate) incremental:
+        Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
 }
 
 impl<P: Clone> From<ServeConfigBuilder<P>> for ServeConfig<P> {

+ 9 - 6
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<Rt, R: RenderHTML + Send>(
-    renderer: &mut IncrementalRenderer<R>,
+pub async fn pre_cache_static_routes<Rt, R: WrapBody + Send + Sync>(
+    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<R: RenderHTML + Send, Rt, W, F: FnOnce(&mut VirtualDom)>(
-    renderer: &mut IncrementalRenderer<R>,
+pub async fn render_route<R: WrapBody + Send + Sync, Rt, W, F: FnOnce(&mut VirtualDom)>(
+    renderer: &mut IncrementalRenderer,
     route: Rt,
     writer: &mut W,
     modify_vdom: F,
+    wrapper: &R,
 ) -> Result<RenderFreshness, IncrementalRendererError>
 where
     Rt: Routable,
@@ -87,6 +89,7 @@ where
             RenderPathProps { path: route },
             writer,
             modify_vdom,
+            wrapper,
         )
         .await
 }

+ 30 - 24
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<R: Write>(&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<R: Write>(&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<R: RenderHTML> {
+pub struct IncrementalRendererConfig {
     static_dir: PathBuf,
     memory_cache_limit: usize,
     invalidate_after: Option<Duration>,
-    render: R,
 }
 
-impl<R: RenderHTML + Default> Default for IncrementalRendererConfig<R> {
+impl Default for IncrementalRendererConfig {
     fn default() -> Self {
-        Self::new(R::default())
+        Self::new()
     }
 }
 
-impl<R: RenderHTML> IncrementalRendererConfig<R> {
+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<R: RenderHTML> IncrementalRendererConfig<R> {
     }
 
     /// Build the incremental renderer.
-    pub fn build(self) -> IncrementalRenderer<R> {
+    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<R: RenderHTML> {
+pub struct IncrementalRenderer {
     static_dir: PathBuf,
     #[allow(clippy::type_complexity)]
     memory_cache:
         Option<lru::LruCache<String, (SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>>,
     invalidate_after: Option<Duration>,
     ssr_renderer: crate::Renderer,
-    render: R,
 }
 
-impl<R: RenderHTML + std::marker::Send> IncrementalRenderer<R> {
+impl IncrementalRenderer {
     /// Get the inner renderer.
     pub fn renderer(&self) -> &crate::Renderer {
         &self.ssr_renderer
@@ -141,8 +137,8 @@ impl<R: RenderHTML + std::marker::Send> IncrementalRenderer<R> {
     }
 
     /// Create a new incremental renderer builder.
-    pub fn builder(renderer: R) -> IncrementalRendererConfig<R> {
-        IncrementalRendererConfig::new(renderer)
+    pub fn builder() -> IncrementalRendererConfig {
+        IncrementalRendererConfig::new()
     }
 
     /// Remove a route from the cache.
@@ -168,13 +164,14 @@ impl<R: RenderHTML + std::marker::Send> IncrementalRenderer<R> {
         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<P>) -> Element,
         props: P,
         output: &'a mut (impl AsyncWrite + Unpin + Send),
         modify_vdom: impl FnOnce(&mut VirtualDom),
+        renderer: &'a R,
     ) -> impl std::future::Future<Output = Result<RenderFreshness, IncrementalRendererError>> + 'a + Send
     {
         let mut html_buffer = WriteBuffer { buffer: Vec::new() };
@@ -185,13 +182,13 @@ impl<R: RenderHTML + std::marker::Send> IncrementalRenderer<R> {
             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<R: RenderHTML + std::marker::Send> IncrementalRenderer<R> {
     }
 
     /// Render a route or get it from cache.
-    pub async fn render<P: 'static>(
+    pub async fn render<P: 'static, R: WrapBody + Send + Sync>(
         &mut self,
         route: String,
         component: fn(Scope<P>) -> Element,
         props: P,
         output: &mut (impl AsyncWrite + Unpin + std::marker::Send),
         modify_vdom: impl FnOnce(&mut VirtualDom),
+        renderer: &R,
     ) -> Result<RenderFreshness, IncrementalRendererError> {
         // check if this route is cached
         if let Some(freshness) = self.search_cache(route.to_string(), output).await? {
@@ -287,7 +285,7 @@ impl<R: RenderHTML + std::marker::Send> IncrementalRenderer<R> {
         } 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<R: RenderHTML + std::marker::Send> IncrementalRenderer<R> {
     }
 
     /// Render a route or get it from cache to a string.
-    pub async fn render_to_string<P: 'static>(
+    pub async fn render_to_string<P: 'static, R: WrapBody + Send + Sync>(
         &mut self,
         route: String,
         component: fn(Scope<P>) -> Element,
         props: P,
         output: &mut String,
         modify_vdom: impl FnOnce(&mut VirtualDom),
+        renderer: &R,
     ) -> Result<RenderFreshness, IncrementalRendererError> {
         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
         }
     }