diff --git a/Cargo.toml b/Cargo.toml index f4ad80086..e2e427043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "packages/server/examples/axum-hello-world", "packages/server/examples/salvo-hello-world", "packages/server/examples/warp-hello-world", + "packages/server/examples/axum-router", "docs/guide", ] diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 54131a13a..b027386c5 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -29,6 +29,7 @@ pub struct RouterProps<'a> { pub active_class: Option<&'a str>, /// Set the initial url. + #[props(!optional, into)] pub initial_url: Option, } diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index ee0a826e9..fa0e0592a 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -17,6 +17,7 @@ http-body = { version = "0.4.5", optional = true } axum = { version = "0.6.1", optional = true } tower-http = { version = "0.4.0", optional = true, features = ["fs"] } hyper = { version = "0.14.25", optional = true } +axum-macros = "0.3.7" # salvo salvo = { version = "0.37.7", optional = true, features = ["serve-static"] } @@ -29,6 +30,7 @@ log = "0.4.17" once_cell = "1.17.1" thiserror = "1.0.40" tokio = { version = "1.27.0", features = ["full"], optional = true } +object-pool = "0.5.4" [features] default = [] diff --git a/packages/server/examples/axum-hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml index 54aaf17d3..68da7a0be 100644 --- a/packages/server/examples/axum-hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -8,13 +8,12 @@ edition = "2021" [dependencies] dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } dioxus = { path = "../../../dioxus" } +dioxus-router = { path = "../../../router" } dioxus-server = { path = "../../" } axum = { version = "0.6.12", optional = true } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" -tracing-subscriber = "0.3.16" -tracing = "0.1.37" [features] ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum"] -web = ["dioxus-web"] +web = ["dioxus-web", "dioxus-router/web"] diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index 4d4ab5e78..b4bc76390 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -25,7 +25,9 @@ fn main() { axum::Router::new() .serve_dioxus_application( ServeConfig::new(app, ()).head(r#"Hello World!"#), + None, ) + .with_state(SSRState::default()) .into_make_service(), ) .await diff --git a/packages/server/examples/axum-router/.gitignore b/packages/server/examples/axum-router/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/axum-router/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/axum-router/Cargo.toml b/packages/server/examples/axum-router/Cargo.toml new file mode 100644 index 000000000..d1d8ff81d --- /dev/null +++ b/packages/server/examples/axum-router/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "axum-router" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-router = { path = "../../../router" } +dioxus-server = { path = "../../" } +axum = { version = "0.6.12", optional = true } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" +tower-http = { version = "0.4.0", features = ["fs"], optional = true } +http = { version = "0.2.9", optional = true } + +[features] +ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum", "tower-http", "http"] +web = ["dioxus-web", "dioxus-router/web"] diff --git a/packages/server/examples/axum-router/src/main.rs b/packages/server/examples/axum-router/src/main.rs new file mode 100644 index 000000000..8745aa7f1 --- /dev/null +++ b/packages/server/examples/axum-router/src/main.rs @@ -0,0 +1,141 @@ +//! Run with: +//! +//! ```sh +//! dioxus build --features web +//! cargo run --features ssr +//! ``` + +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_router::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_with_props( + App, + AppProps { route: None }, + dioxus_web::Config::new().hydrate(true), + ); + #[cfg(feature = "ssr")] + { + use axum::extract::State; + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + use tower_http::services::ServeDir; + + // Serve the dist/assets folder with the javascript and WASM files created by the CLI + let serve_dir = ServeDir::new("dist/assets"); + + axum::Server::bind(&addr) + .serve( + axum::Router::new() + // Register server functions + .register_server_fns("") + // Serve the static assets folder + .nest_service("/assets", serve_dir) + // If the path is unknown, render the application + .fallback( + move |uri: http::uri::Uri, State(ssr_state): State| { + let rendered = ssr_state.render( + &ServeConfig::new( + App, + AppProps { + route: Some(format!("http://{addr}{uri}")), + }, + ) + .head(r#"Hello World!"#), + ); + async move { axum::body::Full::from(rendered) } + }, + ) + .with_state(SSRState::default()) + .into_make_service(), + ) + .await + .unwrap(); + }); + } +} + +#[derive(Clone, Debug, Props, PartialEq)] +struct AppProps { + route: Option, +} + +fn App(cx: Scope) -> Element { + cx.render(rsx! { + Router { + initial_url: cx.props.route.clone(), + + Route { to: "/blog", + Link { + to: "/", + "Go to counter" + } + table { + tbody { + for _ in 0..100 { + tr { + for _ in 0..100 { + td { "hello world??" } + } + } + } + } + } + }, + // Fallback + Route { to: "", + Counter {} + }, + } + }) +} + +fn Counter(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + Link { + to: "/blog", + "Go to blog" + } + div{ + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + } + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml index 1ebe56dfc..9c81ee2a2 100644 --- a/packages/server/examples/salvo-hello-world/Cargo.toml +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -11,8 +11,6 @@ dioxus = { path = "../../../dioxus" } dioxus-server = { path = "../../" } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" -tracing-subscriber = "0.3.16" -tracing = "0.1.37" salvo = { version = "0.37.9", optional = true } [features] diff --git a/packages/server/examples/warp-hello-world/Cargo.toml b/packages/server/examples/warp-hello-world/Cargo.toml index 122eddf36..7bb9aa23f 100644 --- a/packages/server/examples/warp-hello-world/Cargo.toml +++ b/packages/server/examples/warp-hello-world/Cargo.toml @@ -11,8 +11,6 @@ dioxus = { path = "../../../dioxus" } dioxus-server = { path = "../../" } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" -tracing-subscriber = "0.3.16" -tracing = "0.1.37" warp = { version = "0.3.3", optional = true } [features] diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 9d99a697d..5eaff9bfc 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -2,6 +2,7 @@ use std::{error::Error, sync::Arc}; use axum::{ body::{self, Body, BoxBody, Full}, + extract::{FromRef, State}, http::{HeaderMap, Request, Response, StatusCode}, response::IntoResponse, routing::{get, post}, @@ -11,7 +12,7 @@ use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; use crate::{ - dioxus_ssr_html, + render::SSRState, serve::ServeConfig, server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, }; @@ -21,10 +22,15 @@ pub trait DioxusRouterExt { fn serve_dioxus_application( self, cfg: ServeConfig

, + server_fn_route: Option<&'static str>, ) -> Self; } -impl DioxusRouterExt for Router { +impl DioxusRouterExt for Router +where + SSRState: FromRef, + S: Send + Sync + Clone + 'static, +{ fn register_server_fns(self, server_fn_route: &'static str) -> Self { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { @@ -43,17 +49,18 @@ impl DioxusRouterExt for Router { fn serve_dioxus_application( self, cfg: ServeConfig

, + server_fn_route: Option<&'static str>, ) -> Self { use tower_http::services::ServeDir; // Serve the dist folder and the index.html file let serve_dir = ServeDir::new("dist"); - self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) + self.register_server_fns(server_fn_route.unwrap_or_default()) .route( "/", - get(move || { - let rendered = dioxus_ssr_html(&cfg); + get(move |State(ssr_state): State| { + let rendered = ssr_state.render(&cfg); async move { Full::from(rendered) } }), ) diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 1cdd2f786..9141c7199 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -3,6 +3,8 @@ use dioxus_core::prelude::*; mod adapters; #[cfg(feature = "ssr")] +pub mod render; +#[cfg(feature = "ssr")] mod serve; mod server_fn; @@ -14,57 +16,10 @@ pub mod prelude { #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; #[cfg(feature = "ssr")] + pub use crate::render::*; + #[cfg(feature = "ssr")] pub use crate::serve::ServeConfig; pub use crate::server_fn::{DioxusServerContext, ServerFn}; pub use server_fn::{self, ServerFn as _, ServerFnError}; pub use server_macro::*; } - -#[cfg(feature = "ssr")] -fn dioxus_ssr_html(cfg: &serve::ServeConfig

) -> String { - use prelude::ServeConfig; - - let ServeConfig { - app, - application_name, - base_path, - head, - props, - .. - } = cfg; - - let application_name = application_name.unwrap_or("dioxus"); - - let mut vdom = VirtualDom::new_with_props(*app, props.clone()); - let _ = vdom.rebuild(); - let renderered = dioxus_ssr::pre_render(&vdom); - let base_path = base_path.unwrap_or("."); - let head = head.unwrap_or( - r#"Dioxus Application - - - "#, - ); - format!( - r#" - - - - {head} - - -

- {renderered} -
- - -"# - ) -} diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs new file mode 100644 index 000000000..5c15bd097 --- /dev/null +++ b/packages/server/src/render.rs @@ -0,0 +1,92 @@ +use std::fmt::Write; +use std::sync::Arc; + +use dioxus_core::VirtualDom; +use dioxus_ssr::Renderer; + +use crate::prelude::ServeConfig; + +fn dioxus_ssr_html(cfg: &ServeConfig

, renderer: &mut Renderer) -> String { + let ServeConfig { + app, + application_name, + base_path, + head, + props, + .. + } = cfg; + + let application_name = application_name.unwrap_or("dioxus"); + let mut vdom = VirtualDom::new_with_props(*app, props.clone()); + let _ = vdom.rebuild(); + let base_path = base_path.unwrap_or("."); + let head = head.unwrap_or( + r#"Dioxus Application + + + "#, + ); + + let mut html = String::new(); + + if let Err(err) = write!( + &mut html, + r#" + + + {head} + + +

"# + ) { + eprintln!("Failed to write to html: {}", err); + } + + let _ = renderer.render_to(&mut html, &vdom); + + if let Err(err) = write!( + &mut html, + r#"
+ + + "# + ) { + eprintln!("Failed to write to html: {}", err); + } + + html +} + +#[derive(Clone)] +pub struct SSRState { + // We keep a cache 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)), + } + } +} + +impl SSRState { + pub fn render(&self, cfg: &ServeConfig

) -> String { + let mut renderer = self.renderers.pull(pre_renderer); + dioxus_ssr_html(cfg, &mut renderer) + } +} + +fn pre_renderer() -> Renderer { + let mut renderer = Renderer::default(); + renderer.pre_render = true; + renderer +} diff --git a/packages/server/src/serve.rs b/packages/server/src/serve.rs index 9cca17a6b..65f0871ae 100644 --- a/packages/server/src/serve.rs +++ b/packages/server/src/serve.rs @@ -5,7 +5,6 @@ pub struct ServeConfig { pub(crate) app: Component

, pub(crate) props: P, pub(crate) application_name: Option<&'static str>, - pub(crate) server_fn_route: Option<&'static str>, pub(crate) base_path: Option<&'static str>, pub(crate) head: Option<&'static str>, } @@ -17,7 +16,6 @@ impl ServeConfig

{ app, props, application_name: None, - server_fn_route: None, base_path: None, head: None, } @@ -29,12 +27,6 @@ impl ServeConfig

{ self } - /// Set the base route all server functions will be served under - pub fn server_fn_route(mut self, server_fn_route: &'static str) -> Self { - self.server_fn_route = Some(server_fn_route); - self - } - /// Set the path the WASM application will be served under pub fn base_path(mut self, base_path: &'static str) -> Self { self.base_path = Some(base_path); diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 5b6fa8fb2..d9c312ac7 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -3,7 +3,7 @@ use crate::cache::StringCache; use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn}; use std::collections::HashMap; use std::fmt::Write; -use std::rc::Rc; +use std::sync::Arc; /// A virtualdom renderer that caches the templates it has seen for faster rendering #[derive(Default)] @@ -25,7 +25,7 @@ pub struct Renderer { pub skip_components: bool, /// A cache of templates that have been rendered - template_cache: HashMap<&'static str, Rc>, + template_cache: HashMap<&'static str, Arc>, } impl Renderer { @@ -67,7 +67,7 @@ impl Renderer { let entry = self .template_cache .entry(template.template.get().name) - .or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap())) + .or_insert_with(|| Arc::new(StringCache::from_template(template).unwrap())) .clone(); for segment in entry.segments.iter() {