From 1704ee0068b1b0afa9444627f49965839a78ce1f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 22 Jun 2023 16:51:48 -0700 Subject: [PATCH] router/fullstack/ssr intigration --- packages/fullstack/Cargo.toml | 6 +- .../examples/axum-router/src/main.rs | 68 +------ .../fullstack/src/adapters/axum_adapter.rs | 22 ++- .../fullstack/src/adapters/salvo_adapter.rs | 2 +- .../fullstack/src/adapters/warp_adapter.rs | 2 +- packages/fullstack/src/hot_reload.rs | 5 +- packages/fullstack/src/incremental.rs | 0 packages/fullstack/src/lib.rs | 6 +- packages/fullstack/src/render.rs | 106 +++++++--- packages/fullstack/src/serve_config.rs | 181 +++++++++++++++++- packages/fullstack/src/server_context.rs | 2 +- packages/router/Cargo.toml | 5 +- packages/router/benches/incremental.rs | 2 +- packages/router/examples/static_generation.rs | 2 +- packages/router/src/components/router.rs | 25 +++ packages/router/src/incremental.rs | 104 ++++++++++ packages/router/src/lib.rs | 5 +- packages/router/src/routable.rs | 6 +- packages/ssr/Cargo.toml | 6 +- .../src/ssr.rs => ssr/src/incremental.rs} | 154 +++++++-------- packages/ssr/src/lib.rs | 1 + 21 files changed, 507 insertions(+), 203 deletions(-) delete mode 100644 packages/fullstack/src/incremental.rs create mode 100644 packages/router/src/incremental.rs rename packages/{router/src/ssr.rs => ssr/src/incremental.rs} (79%) diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 89ce8f6ae..2fa4d2015 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -30,13 +30,13 @@ salvo = { version = "0.37.7", optional = true, features = ["serve-static", "ws"] serde = "1.0.159" # Dioxus + SSR -dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus = { path = "../dioxus", version = "^0.3.0" } dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true } hyper = { version = "0.14.25", optional = true } http = { version = "0.2.9", optional = true } # Router Intigration -dioxus-router = { path = "../router", version = "^0.3.0", optional = true } +dioxus-router = { path = "../router", version = "^0.3.0", features = ["ssr"], optional = true } log = "0.4.17" once_cell = "1.17.1" @@ -59,7 +59,7 @@ dioxus-hot-reload = { path = "../hot-reload" } web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } [features] -default = ["hot-reload", "default-tls"] +default = ["hot-reload", "default-tls", "router"] router = ["dioxus-router"] hot-reload = ["serde_json", "tokio-stream", "futures-util"] warp = ["dep:warp", "http-body", "ssr"] diff --git a/packages/fullstack/examples/axum-router/src/main.rs b/packages/fullstack/examples/axum-router/src/main.rs index 81ea31ddb..4947351f7 100644 --- a/packages/fullstack/examples/axum-router/src/main.rs +++ b/packages/fullstack/examples/axum-router/src/main.rs @@ -14,8 +14,8 @@ use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] dioxus_web::launch_with_props( - App, - AppProps { route: None }, + Router, + Default::default(), dioxus_web::Config::new().hydrate(true), ); #[cfg(feature = "ssr")] @@ -33,11 +33,6 @@ fn main() { true })); - use axum::extract::State; - use std::str::FromStr; - - PostServerData::register().unwrap(); - GetServerData::register().unwrap(); tokio::runtime::Runtime::new() .unwrap() .block_on(async move { @@ -46,28 +41,12 @@ fn main() { axum::Server::bind(&addr) .serve( axum::Router::new() - // Serve the dist/assets folder with the javascript and WASM files created by the CLI - .serve_static_assets("./dist") - // Register server functions - .register_server_fns("") - // Connect to the hot reload server - .connect_hot_reload() - // If the path is unknown, render the application - .fallback( - move |uri: http::uri::Uri, State(ssr_state): State| { - let rendered = ssr_state.render( - &ServeConfigBuilder::new( - App, - AppProps { - route: Route::from_str(&uri.to_string()).ok(), - }, - ) - .build(), - ); - async move { axum::body::Full::from(rendered) } - }, + .serve_dioxus_application( + "", + ServeConfigBuilder::new_with_router( + dioxus_fullstack::prelude::FullstackRouterConfig::::default()).incremental(IncrementalRendererConfig::default()) + .build(), ) - .with_state(SSRState::default()) .into_make_service(), ) .await @@ -87,10 +66,7 @@ enum Route { #[inline_props] fn Blog(cx: Scope) -> Element { render! { - Link { - target: Route::Home {}, - "Go to counter" - } + Link { target: Route::Home {}, "Go to counter" } table { tbody { for _ in 0..100 { @@ -105,38 +81,14 @@ fn Blog(cx: Scope) -> Element { } } -#[derive(Clone, Debug, Props, PartialEq, Serialize, Deserialize)] -struct AppProps { - route: Option, -} - -fn App(cx: Scope) -> Element { - #[cfg(feature = "ssr")] - let initial_route = cx.props.route.clone().unwrap_or(Route::Home {}); - cx.render(rsx! { - Router { - config: move || RouterConfig::default().history({ - #[cfg(feature = "ssr")] - let history = MemoryHistory::with_initial_path(initial_route); - #[cfg(feature = "web")] - let history = WebHistory::default(); - history - }) - } - }) -} - #[inline_props] fn Home(cx: Scope) -> Element { let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { - Link { - target: Route::Blog {} - "Go to blog" - } - div{ + Link { target: Route::Blog {}, "Go to blog" } + div { h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } diff --git a/packages/fullstack/src/adapters/axum_adapter.rs b/packages/fullstack/src/adapters/axum_adapter.rs index 1ff136d4d..e09c8e0e5 100644 --- a/packages/fullstack/src/adapters/axum_adapter.rs +++ b/packages/fullstack/src/adapters/axum_adapter.rs @@ -63,7 +63,6 @@ use axum::{ routing::{get, post}, Router, }; -use dioxus_core::VirtualDom; use server_fn::{Encoding, Payload, ServerFunctionRegistry}; use std::error::Error; use std::sync::Arc; @@ -311,15 +310,13 @@ where cfg: impl Into>, ) -> Self { let cfg = cfg.into(); + let ssr_state = SSRState::new(&cfg); // Add server functions and render index.html self.serve_static_assets(cfg.assets_path) - .route( - "/", - get(render_handler).with_state((cfg, SSRState::default())), - ) .connect_hot_reload() .register_server_fns(server_fn_route) + .fallback(get(render_handler).with_state((cfg, ssr_state))) } fn connect_hot_reload(self) -> Self { @@ -358,13 +355,18 @@ async fn render_handler( ) -> impl IntoResponse { let (parts, _) = request.into_parts(); let parts: Arc = Arc::new(parts.into()); + let url = parts.uri.path_and_query().unwrap().to_string(); let server_context = DioxusServerContext::new(parts); - let mut vdom = - VirtualDom::new_with_props(cfg.app, cfg.props.clone()).with_root_context(server_context); - let _ = vdom.rebuild(); - let rendered = ssr_state.render_vdom(&vdom, &cfg); - Full::from(rendered) + match ssr_state.render(url, &cfg, |vdom| { + vdom.base_scope().provide_context(server_context); + }) { + Ok(html) => Full::from(html).into_response(), + Err(e) => { + log::error!("Failed to render page: {}", e); + report_err(e).into_response() + } + } } /// A default handler for server functions. It will deserialize the request, call the server function, and serialize the response. diff --git a/packages/fullstack/src/adapters/salvo_adapter.rs b/packages/fullstack/src/adapters/salvo_adapter.rs index 59b7c36ea..2ad9b4069 100644 --- a/packages/fullstack/src/adapters/salvo_adapter.rs +++ b/packages/fullstack/src/adapters/salvo_adapter.rs @@ -282,7 +282,7 @@ impl DioxusRouterExt for Router { self.serve_static_assets(cfg.assets_path) .connect_hot_reload() .register_server_fns(server_fn_path) - .push(Router::with_path("/").get(SSRHandler { cfg })) + .push(Router::with_path("/<**any_path>").get(SSRHandler { cfg })) } fn connect_hot_reload(self) -> Self { diff --git a/packages/fullstack/src/adapters/warp_adapter.rs b/packages/fullstack/src/adapters/warp_adapter.rs index b9f23ae7f..f8e3c6626 100644 --- a/packages/fullstack/src/adapters/warp_adapter.rs +++ b/packages/fullstack/src/adapters/warp_adapter.rs @@ -169,8 +169,8 @@ pub fn serve_dioxus_application>>>, + pub(crate) templates: + Arc>>>, // The channel to send messages to the hot reload thread pub(crate) message_receiver: Receiver>>, } diff --git a/packages/fullstack/src/incremental.rs b/packages/fullstack/src/incremental.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 50ae46642..d4580b768 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -10,8 +10,6 @@ mod props_html; mod adapters; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; -#[cfg(feature = "router")] -mod incremental; #[cfg(feature = "ssr")] mod render; #[cfg(feature = "ssr")] @@ -31,6 +29,8 @@ pub mod prelude { pub use crate::props_html::deserialize_props::get_root_props_from_document; #[cfg(feature = "ssr")] pub use crate::render::SSRState; + #[cfg(all(feature = "router", feature = "ssr"))] + pub use crate::serve_config::FullstackRouterConfig; #[cfg(feature = "ssr")] pub use crate::serve_config::{ServeConfig, ServeConfigBuilder}; #[cfg(feature = "ssr")] @@ -40,5 +40,7 @@ pub mod prelude { #[cfg(feature = "ssr")] pub use crate::server_fn::{ServerFnTraitObj, ServerFunction}; pub use dioxus_server_macro::*; + #[cfg(feature = "ssr")] + pub use dioxus_ssr::incremental::IncrementalRendererConfig; pub use server_fn::{self, ServerFn as _, ServerFnError}; } diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index b56016d5e..72c00e888 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -2,53 +2,93 @@ use std::sync::Arc; -use dioxus_core::VirtualDom; -use dioxus_ssr::Renderer; +use dioxus::prelude::VirtualDom; +use dioxus_ssr::{incremental::IncrementalRendererConfig, Renderer}; -use crate::prelude::ServeConfig; +use crate::prelude::*; +use dioxus::prelude::*; + +enum SsrRendererPool { + Renderer(object_pool::Pool), + Incremental( + object_pool::Pool< + dioxus_ssr::incremental::IncrementalRenderer< + crate::serve_config::EmptyIncrementalRenderTemplate, + >, + >, + ), +} + +impl SsrRendererPool { + fn render_to( + &self, + cfg: &ServeConfig

, + route: String, + component: Component

, + props: P, + to: &mut String, + modify_vdom: impl FnOnce(&mut VirtualDom), + ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { + match self { + Self::Renderer(pool) => { + let mut vdom = VirtualDom::new_with_props(component, props); + + let _ = vdom.rebuild(); + let mut renderer = pool.pull(pre_renderer); + renderer.render_to(to, &vdom)?; + } + Self::Incremental(pool) => { + let mut renderer = pool.pull(|| incremental_pre_renderer(cfg)); + renderer.render_to_string(route, component, props, to, modify_vdom)?; + } + } + Ok(()) + } +} /// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders. #[derive(Clone)] pub struct SSRState { // We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move - renderers: Arc>, -} - -impl Default for SSRState { - fn default() -> Self { - Self { - renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)), - } - } + renderers: Arc, } impl SSRState { - /// Render the application to HTML. - pub fn render(&self, cfg: &ServeConfig

) -> String { - let ServeConfig { app, props, .. } = cfg; + pub(crate) fn new(cfg: &ServeConfig

) -> Self { + if cfg.incremental.is_some() { + return Self { + renderers: Arc::new(SsrRendererPool::Incremental(object_pool::Pool::new( + 10, + || incremental_pre_renderer(cfg), + ))), + }; + } - let mut vdom = VirtualDom::new_with_props(*app, props.clone()); - - let _ = vdom.rebuild(); - - self.render_vdom(&vdom, cfg) + Self { + renderers: Arc::new(SsrRendererPool::Renderer(object_pool::Pool::new( + 10, + pre_renderer, + ))), + } } - /// Render a VirtualDom to HTML. - pub fn render_vdom( + /// Render the application to HTML. + pub fn render( &self, - vdom: &VirtualDom, + route: String, cfg: &ServeConfig

, - ) -> String { - let ServeConfig { index, .. } = cfg; + modify_vdom: impl FnOnce(&mut VirtualDom), + ) -> Result { + let ServeConfig { app, props, .. } = cfg; - let mut renderer = self.renderers.pull(pre_renderer); + let ServeConfig { index, .. } = cfg; let mut html = String::new(); html += &index.pre_main; - let _ = renderer.render_to(&mut html, vdom); + self.renderers + .render_to(cfg, route, *app, props.clone(), &mut html, modify_vdom)?; // serialize the props let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html); @@ -89,12 +129,22 @@ impl SSRState { html += &index.post_main; - html + Ok(html) } } fn pre_renderer() -> Renderer { let mut renderer = Renderer::default(); renderer.pre_render = true; + 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(); + renderer.renderer_mut().pre_render = true; renderer } diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index b7b29e9fb..7c8e9ace0 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -1,10 +1,11 @@ +#![allow(non_snake_case)] //! Configeration for how to serve a Dioxus application use std::fs::File; use std::io::Read; use std::path::PathBuf; -use dioxus_core::Component; +use dioxus::prelude::*; /// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`]. #[derive(Clone)] @@ -14,6 +15,164 @@ pub struct ServeConfigBuilder { pub(crate) root_id: Option<&'static str>, pub(crate) index_path: Option<&'static str>, pub(crate) assets_path: Option<&'static str>, + #[cfg(feature = "router")] + pub(crate) incremental: Option< + std::sync::Arc< + dioxus_ssr::incremental::IncrementalRendererConfig, + >, + >, +} + +#[cfg(feature = "router")] +fn default_external_navigation_handler() -> fn(Scope) -> Element +where + R: dioxus_router::prelude::Routable, + ::Err: std::fmt::Display, +{ + dioxus_router::prelude::FailureExternalNavigation:: +} + +#[cfg(feature = "router")] +/// The configeration for the router +#[derive(Props, serde::Serialize, serde::Deserialize)] +pub struct FullstackRouterConfig +where + R: dioxus_router::prelude::Routable, + ::Err: std::fmt::Display, +{ + #[serde(skip)] + #[serde(default = "default_external_navigation_handler::")] + failure_external_navigation: fn(Scope) -> Element, + scroll_restoration: bool, + #[serde(skip)] + phantom: std::marker::PhantomData, +} + +#[cfg(feature = "router")] +impl Clone for FullstackRouterConfig +where + R: dioxus_router::prelude::Routable, + ::Err: std::fmt::Display, +{ + fn clone(&self) -> Self { + Self { + failure_external_navigation: self.failure_external_navigation, + scroll_restoration: self.scroll_restoration, + phantom: std::marker::PhantomData, + } + } +} + +#[cfg(feature = "router")] +impl Copy for FullstackRouterConfig +where + R: dioxus_router::prelude::Routable, + ::Err: std::fmt::Display, +{ +} + +#[cfg(feature = "router")] +impl Default for FullstackRouterConfig +where + R: dioxus_router::prelude::Routable, + ::Err: std::fmt::Display, +{ + fn default() -> Self { + Self { + failure_external_navigation: dioxus_router::prelude::FailureExternalNavigation::, + scroll_restoration: true, + phantom: std::marker::PhantomData, + } + } +} + +#[cfg(feature = "router")] +fn RouteWithCfg(cx: Scope>) -> Element +where + R: dioxus_router::prelude::Routable, + ::Err: std::fmt::Display, +{ + use dioxus_router::prelude::RouterConfig; + + #[cfg(feature = "ssr")] + let context: crate::prelude::DioxusServerContext = cx + .consume_context() + .expect("RouteWithCfg should be served by dioxus fullstack"); + + let cfg = *cx.props; + render! { + dioxus_router::prelude::GenericRouter:: { + config: move || { + RouterConfig::default() + .failure_external_navigation(cfg.failure_external_navigation) + .history({ + #[cfg(feature = "ssr")] + let history = dioxus_router::prelude::MemoryHistory::with_initial_path( + context + .request_parts() + .uri + .to_string() + .parse() + .unwrap_or_else(|err| { + log::error!("Failed to parse uri: {}", err); + "/" + .parse() + .unwrap_or_else(|err| { + panic!("Failed to parse uri: {}", err); + }) + }), + ); + #[cfg(not(feature = "ssr"))] + let history = dioxus_router::prelude::WebHistory::new( + None, + cfg.scroll_restoration, + ); + history + }) + }, + } + } +} + +#[cfg(feature = "router")] +/// A template for incremental rendering that does nothing. +#[derive(Default, Clone)] +pub struct EmptyIncrementalRenderTemplate; + +#[cfg(feature = "router")] +impl dioxus_ssr::incremental::RenderHTML for EmptyIncrementalRenderTemplate { + fn render_after_body( + &self, + _: &mut R, + ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { + Ok(()) + } + + fn render_before_body( + &self, + _: &mut R, + ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { + Ok(()) + } +} + +#[cfg(feature = "router")] +impl ServeConfigBuilder> +where + R: dioxus_router::prelude::Routable, + ::Err: std::fmt::Display, +{ + /// Create a new ServeConfigBuilder to serve a router on the server. + pub fn new_with_router(cfg: FullstackRouterConfig) -> Self { + Self { + app: RouteWithCfg::, + props: cfg, + root_id: None, + index_path: None, + assets_path: None, + incremental: None, + } + } } impl ServeConfigBuilder

{ @@ -25,9 +184,21 @@ impl ServeConfigBuilder

{ root_id: None, index_path: None, assets_path: None, + #[cfg(feature = "router")] + incremental: None, } } + #[cfg(feature = "router")] + /// Enable incremental static generation + pub fn incremental( + mut self, + cfg: dioxus_ssr::incremental::IncrementalRendererConfig, + ) -> Self { + self.incremental = Some(std::sync::Arc::new(cfg)); + self + } + /// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html) pub fn index_path(mut self, index_path: &'static str) -> Self { self.index_path = Some(index_path); @@ -64,6 +235,8 @@ impl ServeConfigBuilder

{ props: self.props, index, assets_path, + #[cfg(feature = "router")] + incremental: self.incremental, } } } @@ -106,6 +279,12 @@ pub struct ServeConfig { pub(crate) props: P, pub(crate) index: IndexHtml, pub(crate) assets_path: &'static str, + #[cfg(feature = "router")] + pub(crate) incremental: Option< + std::sync::Arc< + dioxus_ssr::incremental::IncrementalRendererConfig, + >, + >, } impl From> for ServeConfig

{ diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index f225526ec..d78052fef 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/fullstack/src/server_context.rs @@ -1,4 +1,4 @@ -use dioxus_core::ScopeState; +use dioxus::prelude::ScopeState; /// A trait for an object that contains a server context pub trait HasServerContext { diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index bebc5001e..ad5b30052 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -24,13 +24,10 @@ web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration" js-sys = { version = "0.3.63", optional = true } gloo-utils = { version = "0.1.6", optional = true } dioxus-ssr = { path = "../ssr", optional = true } -lru = { version = "0.10.0", optional = true } -radix_trie = { version = "0.2.1", optional = true } -rustc-hash = "1.1.0" [features] default = ["web", "ssr"] -ssr = ["dioxus-ssr", "lru", "radix_trie"] +ssr = ["dioxus-ssr"] wasm_test = [] serde = ["dep:serde", "gloo-utils/serde"] web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"] diff --git a/packages/router/benches/incremental.rs b/packages/router/benches/incremental.rs index eba28b91a..5706bfd3a 100644 --- a/packages/router/benches/incremental.rs +++ b/packages/router/benches/incremental.rs @@ -6,7 +6,7 @@ use dioxus::prelude::*; use dioxus_router::prelude::*; use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use dioxus_router::ssr::{DefaultRenderer, IncrementalRenderer}; +use dioxus_router::incremental::{DefaultRenderer, IncrementalRenderer}; use dioxus_ssr::Renderer; pub fn criterion_benchmark(c: &mut Criterion) { diff --git a/packages/router/examples/static_generation.rs b/packages/router/examples/static_generation.rs index 0aa97cd80..97a88f1e7 100644 --- a/packages/router/examples/static_generation.rs +++ b/packages/router/examples/static_generation.rs @@ -5,7 +5,7 @@ use std::time::Duration; use dioxus::prelude::*; use dioxus_router::prelude::*; -use dioxus_router::ssr::{DefaultRenderer, IncrementalRendererConfig}; +use dioxus_ssr::incremental::{DefaultRenderer, IncrementalRendererConfig}; fn main() { let mut renderer = IncrementalRendererConfig::new(DefaultRenderer { diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 7c6f5f0a4..54ac74173 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -66,6 +66,31 @@ where config: RouterConfigFactory, } +#[cfg(not(feature = "serde"))] +impl Default for GenericRouterProps +where + ::Err: std::fmt::Display, +{ + fn default() -> Self { + Self { + config: RouterConfigFactory::default(), + } + } +} + +#[cfg(feature = "serde")] +impl Default for GenericRouterProps +where + ::Err: std::fmt::Display, + R: serde::Serialize + serde::de::DeserializeOwned, +{ + fn default() -> Self { + Self { + config: RouterConfigFactory::default(), + } + } +} + #[cfg(not(feature = "serde"))] impl PartialEq for GenericRouterProps where diff --git a/packages/router/src/incremental.rs b/packages/router/src/incremental.rs new file mode 100644 index 000000000..3c8d1e3e6 --- /dev/null +++ b/packages/router/src/incremental.rs @@ -0,0 +1,104 @@ +//! Exentsions to the incremental renderer to support pre-caching static routes. +use std::str::FromStr; + +use dioxus::prelude::*; +use dioxus_ssr::incremental::{IncrementalRenderer, IncrementalRendererError, RenderHTML}; + +use crate::prelude::*; + +trait IncrementalRendererRouterExt { + /// Pre-cache all static routes. + fn pre_cache_static_routes(&mut self) -> Result<(), IncrementalRendererError> + where + Rt: Routable, + ::Err: std::fmt::Display; + + /// Render a route to a writer. + fn render_route( + &mut self, + route: Rt, + writer: &mut W, + modify_vdom: F, + ) -> Result<(), IncrementalRendererError> + where + Rt: Routable, + ::Err: std::fmt::Display, + W: std::io::Write; +} + +impl IncrementalRendererRouterExt for IncrementalRenderer { + fn pre_cache_static_routes(&mut self) -> Result<(), IncrementalRendererError> + where + Rt: Routable, + ::Err: std::fmt::Display, + { + for route in Rt::SITE_MAP + .iter() + .flat_map(|seg| seg.flatten().into_iter()) + { + // check if this is a static segment + let mut is_static = true; + let mut full_path = String::new(); + for segment in &route { + match segment { + SegmentType::Static(s) => { + full_path += "/"; + full_path += s; + } + _ => { + // skip routes with any dynamic segments + is_static = false; + break; + } + } + } + + if is_static { + match Rt::from_str(&full_path) { + Ok(route) => { + self.render_route(route, &mut std::io::sink(), |_| {})?; + } + Err(e) => { + log::error!("Error pre-caching static route: {}", e); + } + } + } + } + + Ok(()) + } + + fn render_route( + &mut self, + route: Rt, + writer: &mut W, + modify_vdom: F, + ) -> Result<(), IncrementalRendererError> + where + Rt: Routable, + ::Err: std::fmt::Display, + W: std::io::Write, + { + #[inline_props] + fn RenderPath(cx: Scope, path: R) -> Element + where + R: Routable, + ::Err: std::fmt::Display, + { + let path = path.clone(); + render! { + GenericRouter:: { + config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path)) + } + } + } + + self.render( + route.to_string(), + RenderPath, + RenderPathProps { path: route }, + writer, + modify_vdom, + ) + } +} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index c145fad61..1327f9a93 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -7,7 +7,7 @@ pub mod navigation; pub mod routable; #[cfg(feature = "ssr")] -pub mod ssr; +pub mod incremental; /// Components interacting with the router. pub mod components { @@ -62,6 +62,9 @@ pub mod prelude { pub use crate::router_cfg::RouterConfig; pub use dioxus_router_macro::Routable; + #[cfg(feature = "ssr")] + pub use dioxus_ssr::incremental::IncrementalRendererConfig; + #[doc(hidden)] /// A component with props used in the macro pub trait HasProps { diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index b83560542..4ae4563c3 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -94,9 +94,9 @@ impl> FromRouteSegments for I { } /// Something that can be: -/// 1) Converted from a route -/// 2) Converted to a route -/// 3) Rendered as a component +/// 1. Converted from a route +/// 2. Converted to a route +/// 3. Rendered as a component /// /// This trait can be derived using the `#[derive(Routable)]` macro pub trait Routable: std::fmt::Display + std::str::FromStr + Clone + 'static { diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 02900ca0e..0da2d3ba6 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -15,11 +15,13 @@ keywords = ["dom", "ui", "gui", "react", "ssr"] [dependencies] dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] } askama_escape = "0.10.3" +thiserror = "1.0.23" +rustc-hash = "1.1.0" +lru = "0.10.0" +log = "0.4.13" [dev-dependencies] dioxus = { path = "../dioxus", version = "0.3.0" } -thiserror = "1.0.23" -log = "0.4.13" fern = { version = "0.6.0", features = ["colored"] } anyhow = "1.0" argh = "0.1.4" diff --git a/packages/router/src/ssr.rs b/packages/ssr/src/incremental.rs similarity index 79% rename from packages/router/src/ssr.rs rename to packages/ssr/src/incremental.rs index 5aa8ef02f..48e9de861 100644 --- a/packages/router/src/ssr.rs +++ b/packages/ssr/src/incremental.rs @@ -2,15 +2,13 @@ #![allow(non_snake_case)] -use crate::prelude::*; -use dioxus::prelude::*; +use dioxus_core::{Element, Scope, VirtualDom}; use rustc_hash::FxHasher; use std::{ hash::BuildHasherDefault, io::Write, num::NonZeroUsize, path::{Path, PathBuf}, - str::FromStr, time::{Duration, SystemTime}, }; @@ -62,6 +60,7 @@ impl RenderHTML for DefaultRenderer { } /// A configuration for the incremental renderer. +#[derive(Debug, Clone)] pub struct IncrementalRendererConfig { static_dir: PathBuf, memory_cache_limit: usize, @@ -69,9 +68,9 @@ pub struct IncrementalRendererConfig { render: R, } -impl Default for IncrementalRendererConfig { +impl Default for IncrementalRendererConfig { fn default() -> Self { - Self::new(DefaultRenderer::default()) + Self::new(R::default()) } } @@ -112,7 +111,7 @@ impl IncrementalRendererConfig { .map(|limit| lru::LruCache::with_hasher(limit, Default::default())), invalidate_after: self.invalidate_after, render: self.render, - ssr_renderer: dioxus_ssr::Renderer::new(), + ssr_renderer: crate::Renderer::new(), } } } @@ -120,45 +119,74 @@ impl IncrementalRendererConfig { /// An incremental renderer. pub struct IncrementalRenderer { static_dir: PathBuf, + #[allow(clippy::type_complexity)] memory_cache: Option), BuildHasherDefault>>, invalidate_after: Option, - ssr_renderer: dioxus_ssr::Renderer, + ssr_renderer: crate::Renderer, render: R, } impl IncrementalRenderer { + /// Get the inner renderer. + pub fn renderer(&self) -> &crate::Renderer { + &self.ssr_renderer + } + + /// Get the inner renderer mutably. + pub fn renderer_mut(&mut self) -> &mut crate::Renderer { + &mut self.ssr_renderer + } + /// Create a new incremental renderer builder. pub fn builder(renderer: R) -> IncrementalRendererConfig { IncrementalRendererConfig::new(renderer) } + /// Remove a route from the cache. + pub fn invalidate(&mut self, route: &str) { + if let Some(cache) = &mut self.memory_cache { + cache.pop(route); + } + if let Some(path) = self.find_file(route) { + let _ = std::fs::remove_file(path.full_path); + } + } + + /// Remove all routes from the cache. + pub fn invalidate_all(&mut self) { + if let Some(cache) = &mut self.memory_cache { + cache.clear(); + } + // clear the static directory + let _ = std::fs::remove_dir_all(&self.static_dir); + } + fn track_timestamps(&self) -> bool { self.invalidate_after.is_some() } - fn render_and_cache( + fn render_and_cache( &mut self, - route: Rt, + route: String, + comp: fn(Scope

) -> Element, + props: P, output: &mut impl Write, - ) -> Result<(), IncrementalRendererError> - where - Rt: Routable, - ::Err: std::fmt::Display, - { - let route_str = route.to_string(); - let mut vdom = VirtualDom::new_with_props(RenderPath, RenderPathProps { path: route }); + modify_vdom: impl FnOnce(&mut VirtualDom), + ) -> Result<(), IncrementalRendererError> { + let mut vdom = VirtualDom::new_with_props(comp, props); + modify_vdom(&mut vdom); let _ = vdom.rebuild(); let mut html_buffer = WriteBuffer { buffer: Vec::new() }; self.render.render_before_body(&mut html_buffer)?; - self.ssr_renderer.render_to(&mut html_buffer, &mut vdom)?; + self.ssr_renderer.render_to(&mut html_buffer, &vdom)?; self.render.render_after_body(&mut html_buffer)?; let html_buffer = html_buffer.buffer; output.write_all(&html_buffer)?; - self.add_to_cache(route_str, html_buffer) + self.add_to_cache(route, html_buffer) } fn add_to_cache( @@ -181,7 +209,7 @@ impl IncrementalRenderer { fn add_to_memory_cache(&mut self, route: String, html: Vec) { if let Some(cache) = self.memory_cache.as_mut() { - cache.put(route.to_string(), (SystemTime::now(), html)); + cache.put(route, (SystemTime::now(), html)); } } @@ -228,25 +256,39 @@ impl IncrementalRenderer { } /// Render a route or get it from cache. - pub fn render( + pub fn render( &mut self, - route: Rt, + route: String, + component: fn(Scope

) -> Element, + props: P, output: &mut impl Write, - ) -> Result<(), IncrementalRendererError> - where - Rt: Routable, - ::Err: std::fmt::Display, - { + modify_vdom: impl FnOnce(&mut VirtualDom), + ) -> Result<(), IncrementalRendererError> { // check if this route is cached if !self.search_cache(route.to_string(), output)? { // if not, create it - self.render_and_cache(route, output)?; + self.render_and_cache(route, component, props, output, modify_vdom)?; log::trace!("cache miss"); } Ok(()) } + /// Render a route or get it from cache to a string. + pub fn render_to_string( + &mut self, + route: String, + component: fn(Scope

) -> Element, + props: P, + output: &mut String, + modify_vdom: impl FnOnce(&mut VirtualDom), + ) -> Result<(), IncrementalRendererError> { + unsafe { + // SAFETY: The renderer will only write utf8 to the buffer + self.render(route, component, props, output.as_mut_vec(), modify_vdom) + } + } + fn find_file(&self, route: &str) -> Option { let mut file_path = self.static_dir.clone(); for segment in route.split('/') { @@ -300,48 +342,6 @@ impl IncrementalRenderer { file_path.set_extension("html"); file_path } - - /// Pre-cache all static routes. - pub fn pre_cache_static_routes(&mut self) -> Result<(), IncrementalRendererError> - where - Rt: Routable, - ::Err: std::fmt::Display, - { - for route in Rt::SITE_MAP - .iter() - .flat_map(|seg| seg.flatten().into_iter()) - { - // check if this is a static segment - let mut is_static = true; - let mut full_path = String::new(); - for segment in &route { - match segment { - SegmentType::Static(s) => { - full_path += "/"; - full_path += s; - } - _ => { - // skip routes with any dynamic segments - is_static = false; - break; - } - } - } - - if is_static { - match Rt::from_str(&full_path) { - Ok(route) => { - let _ = self.render(route, &mut std::io::sink())?; - } - Err(e) => { - log::error!("Error pre-caching static route: {}", e); - } - } - } - } - - Ok(()) - } } struct WriteBuffer { @@ -375,7 +375,7 @@ impl ValidCachedPath { if value.extension() != Some(std::ffi::OsStr::new("html")) { return None; } - let timestamp = decode_timestamp(&value.file_stem()?.to_str()?)?; + let timestamp = decode_timestamp(value.file_stem()?.to_str()?)?; let full_path = value; Some(Self { full_path, @@ -398,20 +398,6 @@ fn timestamp() -> String { format!("{:x}", timestamp) } -#[inline_props] -fn RenderPath(cx: Scope, path: R) -> Element -where - R: Routable, - ::Err: std::fmt::Display, -{ - let path = path.clone(); - render! { - GenericRouter:: { - config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path)) - } - } -} - /// An error that can occur while rendering a route or retrieving a cached route. #[derive(Debug, thiserror::Error)] pub enum IncrementalRendererError { diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 7d2e87da4..b78bcfeb3 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -2,6 +2,7 @@ mod cache; pub mod config; +pub mod incremental; pub mod renderer; pub mod template; use dioxus_core::{Element, LazyNodes, Scope, VirtualDom};