diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index d182aa415..2e20cdc81 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -44,10 +44,15 @@ anymap = "0.12.1" serde_json = { version = "1.0.95", optional = true } tokio-stream = { version = "0.1.12", features = ["sync"], optional = true } futures-util = { version = "0.3.28", optional = true } +postcard = { version = "1.0.4", features = ["use-std"] } +yazi = "0.1.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { path = "../hot-reload" } +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } + [features] default = ["hot-reload"] hot-reload = ["serde_json", "tokio-stream", "futures-util"] diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index 899247af5..b5553f7a2 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -8,10 +8,15 @@ #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_server::prelude::*; +use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + dioxus_web::launch_with_props( + app, + get_props_from_document().unwrap_or_default(), + dioxus_web::Config::new().hydrate(true), + ); #[cfg(feature = "ssr")] { PostServerData::register().unwrap(); @@ -23,7 +28,10 @@ fn main() { axum::Server::bind(&addr) .serve( axum::Router::new() - .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .serve_dioxus_application( + "", + ServeConfigBuilder::new(app, AppProps { count: 12345 }).build(), + ) .into_make_service(), ) .await @@ -32,8 +40,13 @@ fn main() { } } -fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); +#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] +struct AppProps { + count: i32, +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs index d1e654fee..15a3ebe31 100644 --- a/packages/server/examples/salvo-hello-world/src/main.rs +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -8,10 +8,15 @@ #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_server::prelude::*; +use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + dioxus_web::launch_with_props( + app, + get_props_from_document().unwrap_or_default(), + dioxus_web::Config::new().hydrate(true), + ); #[cfg(feature = "ssr")] { use salvo::prelude::*; @@ -20,8 +25,10 @@ fn main() { tokio::runtime::Runtime::new() .unwrap() .block_on(async move { - let router = - Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ())); + let router = Router::new().serve_dioxus_application( + "", + ServeConfigBuilder::new(app, AppProps { count: 12345 }), + ); Server::new(TcpListener::bind("127.0.0.1:8080")) .serve(router) .await; @@ -29,8 +36,13 @@ fn main() { } } -fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); +#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] +struct AppProps { + count: i32, +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs index 51a627d16..a636dbc58 100644 --- a/packages/server/examples/warp-hello-world/src/main.rs +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -8,10 +8,15 @@ #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_server::prelude::*; +use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + dioxus_web::launch_with_props( + app, + get_props_from_document().unwrap_or_default(), + dioxus_web::Config::new().hydrate(true), + ); #[cfg(feature = "ssr")] { PostServerData::register().unwrap(); @@ -19,14 +24,22 @@ fn main() { tokio::runtime::Runtime::new() .unwrap() .block_on(async move { - let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ())); + let routes = serve_dioxus_application( + "", + ServeConfigBuilder::new(app, AppProps { count: 12345 }), + ); warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; }); } } -fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); +#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] +struct AppProps { + count: i32, +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 2d16e9491..7fc47a8b3 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -184,7 +184,7 @@ pub trait DioxusRouterExt { /// todo!() /// } /// ``` - fn serve_dioxus_application( + fn serve_dioxus_application( self, server_fn_route: &'static str, cfg: impl Into>, @@ -229,7 +229,7 @@ where }) } - fn serve_dioxus_application( + fn serve_dioxus_application( mut self, server_fn_route: &'static str, cfg: impl Into>, @@ -294,7 +294,7 @@ where } } -async fn render_handler( +async fn render_handler( State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, ) -> impl IntoResponse { let rendered = ssr_state.render(&cfg); diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 402a0b0a0..ca11ff9ec 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -173,7 +173,7 @@ pub trait DioxusRouterExt { /// /// fn app(cx: Scope) -> Element {todo!()} /// ``` - fn serve_dioxus_application( + fn serve_dioxus_application( self, server_fn_path: &'static str, cfg: impl Into>, @@ -212,7 +212,7 @@ impl DioxusRouterExt for Router { }) } - fn serve_dioxus_application( + fn serve_dioxus_application( mut self, server_fn_route: &'static str, cfg: impl Into>, @@ -269,7 +269,7 @@ struct SSRHandler { } #[async_trait] -impl Handler for SSRHandler

{ +impl Handler for SSRHandler

{ async fn handle( &self, _req: &mut Request, diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index db88ac2b4..414160045 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -189,7 +189,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R /// todo!() /// } /// ``` -pub fn serve_dioxus_application( +pub fn serve_dioxus_application( server_fn_route: &'static str, cfg: impl Into>, ) -> BoxedFilter<(impl Reply,)> { diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 525b636b3..e9f4ee63f 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -5,6 +5,8 @@ pub use adapters::*; +mod props_html; + mod adapters; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; @@ -23,6 +25,8 @@ pub mod prelude { pub use crate::adapters::salvo_adapter::*; #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; + #[cfg(not(feature = "ssr"))] + pub use crate::props_html::deserialize_props::get_props_from_document; #[cfg(feature = "ssr")] pub use crate::render::SSRState; #[cfg(feature = "ssr")] diff --git a/packages/server/src/props_html/deserialize_props.rs b/packages/server/src/props_html/deserialize_props.rs new file mode 100644 index 000000000..28b206ac5 --- /dev/null +++ b/packages/server/src/props_html/deserialize_props.rs @@ -0,0 +1,37 @@ +use serde::de::DeserializeOwned; + +use super::u16_from_char; + +#[allow(unused)] +pub(crate) fn serde_from_string(string: &str) -> Option { + let decompressed = string + .chars() + .flat_map(|c| { + let u = u16_from_char(c); + let u1 = (u >> 8) as u8; + let u2 = (u & 0xFF) as u8; + [u1, u2].into_iter() + }) + .collect::>(); + let (decompressed, _) = yazi::decompress(&decompressed, yazi::Format::Zlib).unwrap(); + + postcard::from_bytes(&decompressed).ok() +} + +#[cfg(not(feature = "ssr"))] +/// Get the props from the document. This is only available in the browser. +pub fn get_props_from_document() -> Option { + #[cfg(not(target_arch = "wasm32"))] + { + None + } + #[cfg(target_arch = "wasm32")] + { + let attribute = web_sys::window()? + .document()? + .get_element_by_id("dioxus-storage")? + .get_attribute("data-serialized")?; + + serde_from_string(&attribute) + } +} diff --git a/packages/server/src/props_html/mod.rs b/packages/server/src/props_html/mod.rs new file mode 100644 index 000000000..9e9b8721e --- /dev/null +++ b/packages/server/src/props_html/mod.rs @@ -0,0 +1,85 @@ +pub(crate) mod deserialize_props; + +pub(crate) mod serialize_props; + +#[test] +fn serialized_and_deserializes() { + use postcard::to_allocvec; + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] + struct Data { + a: u32, + b: String, + bytes: Vec, + nested: Nested, + } + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] + struct Nested { + a: u32, + b: u16, + c: u8, + } + + for x in 0..10usize { + for y in 0..10 { + let mut as_string = String::new(); + let data = vec![ + Data { + a: x as u32, + b: "hello".to_string(), + bytes: vec![0; x], + nested: Nested { + a: 1, + b: x as u16, + c: 3 + }, + }; + y + ]; + serialize_props::serde_to_writable(&data, &mut as_string).unwrap(); + + println!("{}", as_string); + println!( + "original size: {}", + std::mem::size_of::() * data.len() + ); + println!("serialized size: {}", to_allocvec(&data).unwrap().len()); + println!("compressed size: {}", as_string.len()); + + let decoded: Vec = deserialize_props::serde_from_string(&as_string).unwrap(); + assert_eq!(data, decoded); + } + } +} + +#[test] +fn encodes_and_decodes_bytes() { + for i in 0..(u16::MAX) { + let c = u16_to_char(i); + let i2 = u16_from_char(c); + assert_eq!(i, i2); + } +} + +#[allow(unused)] +pub(crate) fn u16_to_char(u: u16) -> char { + let u = u as u32; + let mapped = if u <= 0xD7FF { + u + } else { + 0xE000 + (u - 0xD7FF) + }; + char::from_u32(mapped).unwrap() +} + +#[allow(unused)] +pub(crate) fn u16_from_char(c: char) -> u16 { + let c = c as u32; + let mapped = if c <= 0xD7FF { + c + } else { + 0xD7FF + (c - 0xE000) + }; + mapped as u16 +} diff --git a/packages/server/src/props_html/serialize_props.rs b/packages/server/src/props_html/serialize_props.rs new file mode 100644 index 000000000..f1282f07f --- /dev/null +++ b/packages/server/src/props_html/serialize_props.rs @@ -0,0 +1,37 @@ +use serde::Serialize; + +use super::u16_to_char; + +#[allow(unused)] +pub(crate) fn serde_to_writable( + value: &T, + mut write_to: impl std::fmt::Write, +) -> std::fmt::Result { + let serialized = postcard::to_allocvec(value).unwrap(); + let compressed = yazi::compress( + &serialized, + yazi::Format::Zlib, + yazi::CompressionLevel::BestSize, + ) + .unwrap(); + for array in compressed.chunks(2) { + let w = if array.len() == 2 { + [array[0], array[1]] + } else { + [array[0], 0] + }; + write_to.write_char(u16_to_char((w[0] as u16) << 8 | (w[1] as u16)))?; + } + Ok(()) +} + +#[cfg(feature = "ssr")] +/// Encode data into a element. This is inteded to be used in the server to send data to the client. +pub(crate) fn encode_in_element( + data: T, + mut write_to: impl std::fmt::Write, +) -> std::fmt::Result { + write_to.write_str(r#""#) +} diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 8328be317..0f61a3a0f 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -24,7 +24,7 @@ impl Default for SSRState { impl SSRState { /// Render the application to HTML. - pub fn render(&self, cfg: &ServeConfig

) -> String { + pub fn render(&self, cfg: &ServeConfig

) -> String { let ServeConfig { app, props, index, .. } = cfg; @@ -41,6 +41,9 @@ impl SSRState { let _ = renderer.render_to(&mut html, &vdom); + // serialize the props + let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html); + html += &index.post_main; html