Prechádzať zdrojové kódy

allow passing root prop data from the server to the client

Evan Almloff 2 rokov pred
rodič
commit
03a2824660

+ 5 - 0
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"]

+ 17 - 4
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<AppProps>) -> Element {
+    let mut count = use_state(cx, || cx.props.count);
     let text = use_state(cx, || "...".to_string());
 
     cx.render(rsx! {

+ 17 - 5
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<AppProps>) -> Element {
+    let mut count = use_state(cx, || cx.props.count);
     let text = use_state(cx, || "...".to_string());
 
     cx.render(rsx! {

+ 17 - 4
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<AppProps>) -> Element {
+    let mut count = use_state(cx, || cx.props.count);
     let text = use_state(cx, || "...".to_string());
 
     cx.render(rsx! {

+ 3 - 3
packages/server/src/adapters/axum_adapter.rs

@@ -184,7 +184,7 @@ pub trait DioxusRouterExt<S> {
     ///     todo!()
     /// }
     /// ```
-    fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
+    fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
         self,
         server_fn_route: &'static str,
         cfg: impl Into<ServeConfig<P>>,
@@ -229,7 +229,7 @@ where
         })
     }
 
-    fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
+    fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
         mut self,
         server_fn_route: &'static str,
         cfg: impl Into<ServeConfig<P>>,
@@ -294,7 +294,7 @@ where
     }
 }
 
-async fn render_handler<P: Clone + Send + Sync + 'static>(
+async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
     State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
 ) -> impl IntoResponse {
     let rendered = ssr_state.render(&cfg);

+ 3 - 3
packages/server/src/adapters/salvo_adapter.rs

@@ -173,7 +173,7 @@ pub trait DioxusRouterExt {
     ///
     /// fn app(cx: Scope) -> Element {todo!()}
     /// ```    
-    fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
+    fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
         self,
         server_fn_path: &'static str,
         cfg: impl Into<ServeConfig<P>>,
@@ -212,7 +212,7 @@ impl DioxusRouterExt for Router {
         })
     }
 
-    fn serve_dioxus_application<P: Clone + Send + Sync + 'static>(
+    fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
         mut self,
         server_fn_route: &'static str,
         cfg: impl Into<ServeConfig<P>>,
@@ -269,7 +269,7 @@ struct SSRHandler<P: Clone> {
 }
 
 #[async_trait]
-impl<P: Clone + Send + Sync + 'static> Handler for SSRHandler<P> {
+impl<P: Clone + serde::Serialize + Send + Sync + 'static> Handler for SSRHandler<P> {
     async fn handle(
         &self,
         _req: &mut Request,

+ 1 - 1
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<P: Clone + Send + Sync + 'static>(
+pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
     server_fn_route: &'static str,
     cfg: impl Into<ServeConfig<P>>,
 ) -> BoxedFilter<(impl Reply,)> {

+ 4 - 0
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")]

+ 37 - 0
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<T: DeserializeOwned>(string: &str) -> Option<T> {
+    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::<Vec<u8>>();
+    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<T: DeserializeOwned>() -> Option<T> {
+    #[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)
+    }
+}

+ 85 - 0
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<u8>,
+        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>() * data.len()
+            );
+            println!("serialized size: {}", to_allocvec(&data).unwrap().len());
+            println!("compressed size: {}", as_string.len());
+
+            let decoded: Vec<Data> = 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
+}

+ 37 - 0
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<T: Serialize>(
+    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<T: Serialize>(
+    data: T,
+    mut write_to: impl std::fmt::Write,
+) -> std::fmt::Result {
+    write_to.write_str(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#)?;
+    serde_to_writable(&data, &mut write_to)?;
+    write_to.write_str(r#"" />"#)
+}

+ 4 - 1
packages/server/src/render.rs

@@ -24,7 +24,7 @@ impl Default for SSRState {
 
 impl SSRState {
     /// Render the application to HTML.
-    pub fn render<P: 'static + Clone>(&self, cfg: &ServeConfig<P>) -> String {
+    pub fn render<P: 'static + Clone + serde::Serialize>(&self, cfg: &ServeConfig<P>) -> 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