router/fullstack/ssr intigration
This commit is contained in:
1 parent
495fd22bfd
commit
1704ee0068
21 files changed
+507
-203
No files matched your search
@@ -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!" }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
@@ -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};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus::prelude::ScopeState;
|
||||
|
||||
/// A trait for an object that contains a server context
|
||||
pub trait HasServerContext {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
@@ -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};
|
||||
|
||||
Reference in new issue
Block a user