Browse Source

fix streaming server functions, and precompress assets in release mode (#2121)

Evan Almloff 1 năm trước cách đây
mục cha
commit
e012d816eb

+ 3 - 2
packages/fullstack/Cargo.toml

@@ -17,7 +17,8 @@ dioxus_server_macro = { workspace = true }
 
 # axum
 axum = { workspace = true, features = ["ws", "macros"], optional = true }
-tower-http = { workspace = true, optional = true, features = ["fs", "compression-gzip"] }
+tower-http = { workspace = true, optional = true, features = ["fs"] }
+async-compression = { version = "0.4.6", features = ["gzip", "tokio"], optional = true }
 
 dioxus-lib = { workspace = true }
 
@@ -73,7 +74,7 @@ desktop = ["dioxus-desktop"]
 mobile = ["dioxus-mobile"]
 default-tls = ["server_fn/default-tls"]
 rustls = ["server_fn/rustls"]
-axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum"]
+axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "async-compression"]
 server = [
     "server_fn/ssr",
     "dioxus_server_macro/server",

+ 1 - 0
packages/fullstack/examples/axum-auth/src/main.rs

@@ -52,6 +52,7 @@ fn main() {
                     .serve_dioxus_application(ServeConfig::builder().build(), || {
                         VirtualDom::new(app)
                     })
+                    .await
                     .layer(
                         axum_session_auth::AuthSessionLayer::<
                             crate::auth::User,

+ 70 - 0
packages/fullstack/src/assets.rs

@@ -0,0 +1,70 @@
+//! Handles pre-compression for any static assets
+
+use std::{ffi::OsString, path::PathBuf, pin::Pin};
+
+use async_compression::tokio::bufread::GzipEncoder;
+use futures_util::Future;
+use tokio::task::JoinSet;
+
+#[allow(unused)]
+pub async fn pre_compress_files(directory: PathBuf) -> tokio::io::Result<()> {
+    // print to stdin encoded gzip data
+    pre_compress_dir(directory).await?;
+    Ok(())
+}
+
+fn pre_compress_dir(
+    path: PathBuf,
+) -> Pin<Box<dyn Future<Output = tokio::io::Result<()>> + Send + Sync>> {
+    Box::pin(async move {
+        let mut entries = tokio::fs::read_dir(&path).await?;
+        let mut set: JoinSet<tokio::io::Result<()>> = JoinSet::new();
+
+        while let Some(entry) = entries.next_entry().await? {
+            set.spawn(async move {
+                if entry.file_type().await?.is_dir() {
+                    if let Err(err) = pre_compress_dir(entry.path()).await {
+                        tracing::error!(
+                            "Failed to pre-compress directory {}: {}",
+                            entry.path().display(),
+                            err
+                        );
+                    }
+                } else if let Err(err) = pre_compress_file(entry.path()).await {
+                    tracing::error!(
+                        "Failed to pre-compress static assets {}: {}",
+                        entry.path().display(),
+                        err
+                    );
+                }
+
+                Ok(())
+            });
+        }
+        while let Some(res) = set.join_next().await {
+            res??;
+        }
+        Ok(())
+    })
+}
+
+async fn pre_compress_file(path: PathBuf) -> tokio::io::Result<()> {
+    let file = tokio::fs::File::open(&path).await?;
+    let stream = tokio::io::BufReader::new(file);
+    let mut encoder = GzipEncoder::new(stream);
+    let new_extension = match path.extension() {
+        Some(ext) => {
+            if ext.to_string_lossy().to_lowercase().ends_with("gz") {
+                return Ok(());
+            }
+            let mut ext = ext.to_os_string();
+            ext.push(".gz");
+            ext
+        }
+        None => OsString::from("gz"),
+    };
+    let output = path.with_extension(new_extension);
+    let mut buffer = tokio::fs::File::create(&output).await?;
+    tokio::io::copy(&mut encoder, &mut buffer).await?;
+    Ok(())
+}

+ 65 - 43
packages/fullstack/src/axum_adapter.rs

@@ -64,6 +64,7 @@ use axum::{
     Router,
 };
 use dioxus_lib::prelude::VirtualDom;
+use futures_util::Future;
 use http::header::*;
 
 use std::sync::Arc;
@@ -149,7 +150,12 @@ pub trait DioxusRouterExt<S> {
     ///     unimplemented!()
     /// }
     /// ```
-    fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self;
+    fn serve_static_assets(
+        self,
+        assets_path: impl Into<std::path::PathBuf>,
+    ) -> impl Future<Output = Self> + Send + Sync
+    where
+        Self: Sized;
 
     /// Serves the Dioxus application. This will serve a complete server side rendered application.
     /// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
@@ -182,7 +188,9 @@ pub trait DioxusRouterExt<S> {
         self,
         cfg: impl Into<ServeConfig>,
         build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
-    ) -> Self;
+    ) -> impl Future<Output = Self> + Send + Sync
+    where
+        Self: Sized;
 }
 
 impl<S> DioxusRouterExt<S> for Router<S>
@@ -206,59 +214,75 @@ where
         self
     }
 
-    fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
+    fn serve_static_assets(
+        mut self,
+        assets_path: impl Into<std::path::PathBuf>,
+    ) -> impl Future<Output = Self> + Send + Sync {
         use tower_http::services::{ServeDir, ServeFile};
 
         let assets_path = assets_path.into();
-
-        // Serve all files in dist folder except index.html
-        let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
-            panic!(
-                "Couldn't read assets directory at {:?}: {}",
-                &assets_path, e
-            )
-        });
-
-        for entry in dir.flatten() {
-            let path = entry.path();
-            if path.ends_with("index.html") {
-                continue;
+        async move {
+            #[cfg(not(debug_assertions))]
+            if let Err(err) = crate::assets::pre_compress_files(assets_path.clone()).await {
+                tracing::error!("Failed to pre-compress static assets: {}", err);
             }
-            let route = path
-                .strip_prefix(&assets_path)
-                .unwrap()
-                .iter()
-                .map(|segment| {
-                    segment.to_str().unwrap_or_else(|| {
-                        panic!("Failed to convert path segment {:?} to string", segment)
+
+            // Serve all files in dist folder except index.html
+            let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
+                panic!(
+                    "Couldn't read assets directory at {:?}: {}",
+                    &assets_path, e
+                )
+            });
+
+            for entry in dir.flatten() {
+                let path = entry.path();
+                if path.ends_with("index.html") {
+                    continue;
+                }
+                let route = path
+                    .strip_prefix(&assets_path)
+                    .unwrap()
+                    .iter()
+                    .map(|segment| {
+                        segment.to_str().unwrap_or_else(|| {
+                            panic!("Failed to convert path segment {:?} to string", segment)
+                        })
                     })
-                })
-                .collect::<Vec<_>>()
-                .join("/");
-            let route = format!("/{}", route);
-            if path.is_dir() {
-                self = self.nest_service(&route, ServeDir::new(path));
-            } else {
-                self = self.nest_service(&route, ServeFile::new(path));
+                    .collect::<Vec<_>>()
+                    .join("/");
+                let route = format!("/{}", route);
+                if path.is_dir() {
+                    self = self.nest_service(&route, ServeDir::new(path).precompressed_gzip());
+                } else {
+                    self = self.nest_service(&route, ServeFile::new(path).precompressed_gzip());
+                }
             }
-        }
 
-        self
+            self
+        }
     }
 
     fn serve_dioxus_application(
         self,
         cfg: impl Into<ServeConfig>,
         build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
-    ) -> Self {
+    ) -> impl Future<Output = Self> + Send + Sync {
         let cfg = cfg.into();
-        let ssr_state = SSRState::new(&cfg);
-
-        // Add server functions and render index.html
-        self.serve_static_assets(cfg.assets_path.clone())
-            .connect_hot_reload()
-            .register_server_fns()
-            .fallback(get(render_handler).with_state((cfg, Arc::new(build_virtual_dom), ssr_state)))
+        async move {
+            let ssr_state = SSRState::new(&cfg);
+
+            // Add server functions and render index.html
+            self.serve_static_assets(cfg.assets_path.clone())
+                .await
+                .connect_hot_reload()
+                .register_server_fns()
+                .fallback(get(render_handler).with_state((
+                    cfg,
+                    Arc::new(build_virtual_dom),
+                    ssr_state,
+                )))
+        }
     }
 
     fn connect_hot_reload(self) -> Self {
@@ -472,7 +496,6 @@ async fn handle_server_fns_inner(
         if let Some(mut service) =
             server_fn::axum::get_server_fn_service(&path_string)
         {
-
             let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)));
             additional_context();
 
@@ -488,7 +511,6 @@ async fn handle_server_fns_inner(
             // actually run the server fn
             let mut res = service.run(req).await;
 
-
             // it it accepts text/html (i.e., is a plain form post) and doesn't already have a
             // Location set, then redirect to Referer
             if accepts_html {

+ 2 - 6
packages/fullstack/src/config.rs

@@ -133,18 +133,14 @@ impl Config {
             #[cfg(not(any(feature = "desktop", feature = "mobile")))]
             let router = router
                 .serve_static_assets(cfg.assets_path.clone())
+                .await
                 .connect_hot_reload()
                 .fallback(get(render_handler).with_state((
                     cfg,
                     Arc::new(build_virtual_dom),
                     ssr_state,
                 )));
-            let router = router
-                .layer(
-                    ServiceBuilder::new()
-                        .layer(tower_http::compression::CompressionLayer::new().gzip(true)),
-                )
-                .into_make_service();
+            let router = router.into_make_service();
             let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
             axum::serve(listener, router).await.unwrap();
         }

+ 2 - 0
packages/fullstack/src/lib.rs

@@ -8,6 +8,8 @@ pub use once_cell;
 
 mod html_storage;
 
+#[cfg(feature = "axum")]
+mod assets;
 #[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
 #[cfg(feature = "axum")]
 mod axum_adapter;

+ 3 - 2
packages/signals/src/reactive_context.rs

@@ -5,7 +5,7 @@ use futures_channel::mpsc::UnboundedReceiver;
 use generational_box::SyncStorage;
 use std::{cell::RefCell, hash::Hash};
 
-use crate::{CopyValue, Readable, Writable};
+use crate::{CopyValue, Writable};
 
 /// A context for signal reads and writes to be directed to
 ///
@@ -26,6 +26,7 @@ impl std::fmt::Display for ReactiveContext {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         #[cfg(debug_assertions)]
         {
+            use crate::Readable;
             if let Ok(read) = self.inner.try_read() {
                 return write!(f, "ReactiveContext created at {}", read.origin);
             }
@@ -58,7 +59,7 @@ impl ReactiveContext {
     pub fn new_with_callback(
         callback: impl FnMut() + Send + Sync + 'static,
         scope: ScopeId,
-        origin: &'static std::panic::Location<'static>,
+        #[allow(unused)] origin: &'static std::panic::Location<'static>,
     ) -> Self {
         let inner = Inner {
             self_: None,