router/fullstack/ssr intigration

This commit is contained in:
Evan Almloff committed 2023-06-22 16:51:48 -07:00
1 parent 495fd22bfd
commit 1704ee0068
21 files changed
+507 -203

No files matched your search

+3 -3
View File
@@ -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"]
@@ -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<SSRState>| {
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::<Route>::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<Route>,
}
fn App(cx: Scope<AppProps>) -> 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!" }
+12 -10
View File
@@ -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<ServeConfig<P>>,
) -> 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<P: Clone + serde::Serialize + Send + Sync + 'static>(
) -> impl IntoResponse {
let (parts, _) = request.into_parts();
let parts: Arc<RequestParts> = 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.
@@ -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 {
@@ -169,8 +169,8 @@ pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'sta
connect_hot_reload()
.or(register_server_fns(server_fn_route))
.or(warp::path::end().and(render_ssr(cfg)))
.or(serve_dir)
.or(render_ssr(cfg))
.boxed()
}
+3 -2
View File
@@ -1,6 +1,6 @@
use std::sync::Arc;
use dioxus_core::Template;
use dioxus::prelude::Template;
use tokio::sync::{
watch::{channel, Receiver},
RwLock,
@@ -9,7 +9,8 @@ use tokio::sync::{
#[derive(Clone)]
pub struct HotReloadState {
// The cache of all templates that have been modified since the last time we checked
pub(crate) templates: Arc<RwLock<std::collections::HashSet<dioxus_core::Template<'static>>>>,
pub(crate) templates:
Arc<RwLock<std::collections::HashSet<dioxus::prelude::Template<'static>>>>,
// The channel to send messages to the hot reload thread
pub(crate) message_receiver: Receiver<Option<Template<'static>>>,
}
Whitespace-only changes.
+4 -2
View File
@@ -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};
}
+78 -28
View File
@@ -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<Renderer>),
Incremental(
object_pool::Pool<
dioxus_ssr::incremental::IncrementalRenderer<
crate::serve_config::EmptyIncrementalRenderTemplate,
>,
>,
),
}
impl SsrRendererPool {
fn render_to<P: Clone + 'static>(
&self,
cfg: &ServeConfig<P>,
route: String,
component: Component<P>,
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<object_pool::Pool<Renderer>>,
}
impl Default for SSRState {
fn default() -> Self {
Self {
renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)),
}
}
renderers: Arc<SsrRendererPool>,
}
impl SSRState {
/// Render the application to HTML.
pub fn render<P: 'static + Clone + serde::Serialize>(&self, cfg: &ServeConfig<P>) -> String {
let ServeConfig { app, props, .. } = cfg;
pub(crate) fn new<P: Clone>(cfg: &ServeConfig<P>) -> 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<P: 'static + Clone + serde::Serialize>(
/// Render the application to HTML.
pub fn render<P: 'static + Clone + serde::Serialize>(
&self,
vdom: &VirtualDom,
route: String,
cfg: &ServeConfig<P>,
) -> String {
let ServeConfig { index, .. } = cfg;
modify_vdom: impl FnOnce(&mut VirtualDom),
) -> Result<String, dioxus_ssr::incremental::IncrementalRendererError> {
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<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();
renderer.renderer_mut().pre_render = true;
renderer
}
+180 -1
View File
@@ -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<P: Clone> {
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<EmptyIncrementalRenderTemplate>,
>,
>,
}
#[cfg(feature = "router")]
fn default_external_navigation_handler<R>() -> fn(Scope) -> Element
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
dioxus_router::prelude::FailureExternalNavigation::<R>
}
#[cfg(feature = "router")]
/// The configeration for the router
#[derive(Props, serde::Serialize, serde::Deserialize)]
pub struct FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
#[serde(skip)]
#[serde(default = "default_external_navigation_handler::<R>")]
failure_external_navigation: fn(Scope) -> Element,
scroll_restoration: bool,
#[serde(skip)]
phantom: std::marker::PhantomData<R>,
}
#[cfg(feature = "router")]
impl<R> Clone for FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::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<R> Copy for FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
}
#[cfg(feature = "router")]
impl<R> Default for FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self {
failure_external_navigation: dioxus_router::prelude::FailureExternalNavigation::<R>,
scroll_restoration: true,
phantom: std::marker::PhantomData,
}
}
}
#[cfg(feature = "router")]
fn RouteWithCfg<R>(cx: Scope<FullstackRouterConfig<R>>) -> Element
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::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::<R> {
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<R: std::io::Write>(
&self,
_: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
Ok(())
}
fn render_before_body<R: std::io::Write>(
&self,
_: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
Ok(())
}
}
#[cfg(feature = "router")]
impl<R> ServeConfigBuilder<FullstackRouterConfig<R>>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
/// Create a new ServeConfigBuilder to serve a router on the server.
pub fn new_with_router(cfg: FullstackRouterConfig<R>) -> Self {
Self {
app: RouteWithCfg::<R>,
props: cfg,
root_id: None,
index_path: None,
assets_path: None,
incremental: None,
}
}
}
impl<P: Clone> ServeConfigBuilder<P> {
@@ -25,9 +184,21 @@ impl<P: Clone> ServeConfigBuilder<P> {
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<EmptyIncrementalRenderTemplate>,
) -> 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<P: Clone> ServeConfigBuilder<P> {
props: self.props,
index,
assets_path,
#[cfg(feature = "router")]
incremental: self.incremental,
}
}
}
@@ -106,6 +279,12 @@ pub struct ServeConfig<P: Clone> {
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<EmptyIncrementalRenderTemplate>,
>,
>,
}
impl<P: Clone> From<ServeConfigBuilder<P>> for ServeConfig<P> {
+1 -1
View File
@@ -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 {
+1 -4
View File
@@ -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"]
+1 -1
View File
@@ -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) {
@@ -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 {
+25
View File
@@ -66,6 +66,31 @@ where
config: RouterConfigFactory<R>,
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> Default for GenericRouterProps<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self {
config: RouterConfigFactory::default(),
}
}
}
#[cfg(feature = "serde")]
impl<R: Routable> Default for GenericRouterProps<R>
where
<R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
fn default() -> Self {
Self {
config: RouterConfigFactory::default(),
}
}
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> PartialEq for GenericRouterProps<R>
where
+104
View File
@@ -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<Rt>(&mut self) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display;
/// Render a route to a writer.
fn render_route<Rt, W, F: FnOnce(&mut VirtualDom)>(
&mut self,
route: Rt,
writer: &mut W,
modify_vdom: F,
) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display,
W: std::io::Write;
}
impl<R: RenderHTML> IncrementalRendererRouterExt for IncrementalRenderer<R> {
fn pre_cache_static_routes<Rt>(&mut self) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::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<Rt, W, F: FnOnce(&mut VirtualDom)>(
&mut self,
route: Rt,
writer: &mut W,
modify_vdom: F,
) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display,
W: std::io::Write,
{
#[inline_props]
fn RenderPath<R>(cx: Scope, path: R) -> Element
where
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
let path = path.clone();
render! {
GenericRouter::<R> {
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
}
}
self.render(
route.to_string(),
RenderPath,
RenderPathProps { path: route },
writer,
modify_vdom,
)
}
}
+4 -1
View File
@@ -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 {
+3 -3
View File
@@ -94,9 +94,9 @@ impl<I: std::iter::FromIterator<String>> 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 {
+4 -2
View File
@@ -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"
@@ -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<R: RenderHTML> {
static_dir: PathBuf,
memory_cache_limit: usize,
@@ -69,9 +68,9 @@ pub struct IncrementalRendererConfig<R: RenderHTML> {
render: R,
}
impl Default for IncrementalRendererConfig<DefaultRenderer> {
impl<R: RenderHTML + Default> Default for IncrementalRendererConfig<R> {
fn default() -> Self {
Self::new(DefaultRenderer::default())
Self::new(R::default())
}
}
@@ -112,7 +111,7 @@ impl<R: RenderHTML> IncrementalRendererConfig<R> {
.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<R: RenderHTML> IncrementalRendererConfig<R> {
/// An incremental renderer.
pub struct IncrementalRenderer<R: RenderHTML> {
static_dir: PathBuf,
#[allow(clippy::type_complexity)]
memory_cache:
Option<lru::LruCache<String, (SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>>,
invalidate_after: Option<Duration>,
ssr_renderer: dioxus_ssr::Renderer,
ssr_renderer: crate::Renderer,
render: R,
}
impl<R: RenderHTML> IncrementalRenderer<R> {
/// 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<R> {
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<Rt>(
fn render_and_cache<P: 'static>(
&mut self,
route: Rt,
route: String,
comp: fn(Scope<P>) -> Element,
props: P,
output: &mut impl Write,
) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::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<R: RenderHTML> IncrementalRenderer<R> {
fn add_to_memory_cache(&mut self, route: String, html: Vec<u8>) {
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<R: RenderHTML> IncrementalRenderer<R> {
}
/// Render a route or get it from cache.
pub fn render<Rt>(
pub fn render<P: 'static>(
&mut self,
route: Rt,
route: String,
component: fn(Scope<P>) -> Element,
props: P,
output: &mut impl Write,
) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::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<P: 'static>(
&mut self,
route: String,
component: fn(Scope<P>) -> 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<ValidCachedPath> {
let mut file_path = self.static_dir.clone();
for segment in route.split('/') {
@@ -300,48 +342,6 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
file_path.set_extension("html");
file_path
}
/// Pre-cache all static routes.
pub fn pre_cache_static_routes<Rt>(&mut self) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::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<R>(cx: Scope, path: R) -> Element
where
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
let path = path.clone();
render! {
GenericRouter::<R> {
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 {
+1
View File
@@ -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};