Prechádzať zdrojové kódy

add an example illistrating intigration with the router

Evan Almloff 2 rokov pred
rodič
commit
bfcb0f6eab

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

+ 1 - 0
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<String>,
 }
 

+ 2 - 0
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 = []

+ 2 - 3
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"]

+ 2 - 0
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#"<title>Hello World!</title>"#),
+                                None,
                             )
+                            .with_state(SSRState::default())
                             .into_make_service(),
                     )
                     .await

+ 2 - 0
packages/server/examples/axum-router/.gitignore

@@ -0,0 +1,2 @@
+dist
+target

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

+ 141 - 0
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<SSRState>| {
+                                    let rendered = ssr_state.render(
+                                        &ServeConfig::new(
+                                            App,
+                                            AppProps {
+                                                route: Some(format!("http://{addr}{uri}")),
+                                            },
+                                        )
+                                        .head(r#"<title>Hello World!</title>"#),
+                                    );
+                                    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<String>,
+}
+
+fn App(cx: Scope<AppProps>) -> 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<String, ServerFnError> {
+    Ok("Hello from the server!".to_string())
+}

+ 0 - 2
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]

+ 0 - 2
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]

+ 12 - 5
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<P: Clone + Send + Sync + 'static>(
         self,
         cfg: ServeConfig<P>,
+        server_fn_route: Option<&'static str>,
     ) -> Self;
 }
 
-impl DioxusRouterExt for Router {
+impl<S> DioxusRouterExt for Router<S>
+where
+    SSRState: FromRef<S>,
+    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<P: Clone + Send + Sync + 'static>(
         self,
         cfg: ServeConfig<P>,
+        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<SSRState>| {
+                    let rendered = ssr_state.render(&cfg);
                     async move { Full::from(rendered) }
                 }),
             )

+ 4 - 49
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<P: 'static + Clone>(cfg: &serve::ServeConfig<P>) -> 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#"<title>Dioxus Application</title>
-  <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
-  <meta name="viewport" content="width=device-width, initial-scale=1" />
-  <meta charset="UTF-8" />"#,
-    );
-    format!(
-        r#"
-    <!DOCTYPE html>
-<html>
-<head>
-  {head}
-</head>
-<body>
-    <div id="main">
-    {renderered}
-    </div>
-         <script type="module">
-    import init from "/{base_path}/assets/dioxus/{application_name}.js";
-    init("/{base_path}/assets/dioxus/{application_name}_bg.wasm").then(wasm => {{
-      if (wasm.__wbindgen_start == undefined) {{
-        wasm.main();
-      }}
-    }});
-  </script>
-</body>
-</html>"#
-    )
-}

+ 92 - 0
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<P: 'static + Clone>(cfg: &ServeConfig<P>, 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#"<title>Dioxus Application</title>
+        <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <meta charset="UTF-8" />"#,
+    );
+
+    let mut html = String::new();
+
+    if let Err(err) = write!(
+        &mut html,
+        r#"
+        <!DOCTYPE html>
+        <html>
+        <head>{head}
+        </head>
+        <body>
+        <div id="main">"#
+    ) {
+        eprintln!("Failed to write to html: {}", err);
+    }
+
+    let _ = renderer.render_to(&mut html, &vdom);
+
+    if let Err(err) = write!(
+        &mut html,
+        r#"</div>
+        <script type="module">
+        import init from "/{base_path}/assets/dioxus/{application_name}.js";
+    init("/{base_path}/assets/dioxus/{application_name}_bg.wasm").then(wasm => {{
+      if (wasm.__wbindgen_start == undefined) {{
+          wasm.main();
+        }}
+    }});
+    </script>
+    </body>
+    </html>"#
+    ) {
+        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<object_pool::Pool<Renderer>>,
+}
+
+impl Default for SSRState {
+    fn default() -> Self {
+        Self {
+            renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)),
+        }
+    }
+}
+
+impl SSRState {
+    pub fn render<P: 'static + Clone>(&self, cfg: &ServeConfig<P>) -> 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
+}

+ 0 - 8
packages/server/src/serve.rs

@@ -5,7 +5,6 @@ pub struct ServeConfig<P: Clone> {
     pub(crate) app: Component<P>,
     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<P: Clone> ServeConfig<P> {
             app,
             props,
             application_name: None,
-            server_fn_route: None,
             base_path: None,
             head: None,
         }
@@ -29,12 +27,6 @@ impl<P: Clone> ServeConfig<P> {
         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);

+ 3 - 3
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<StringCache>>,
+    template_cache: HashMap<&'static str, Arc<StringCache>>,
 }
 
 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() {