Make it easier to provide context to fullstack (#2515)

* pass context providers into server functions

* add an example for FromContext

* clean up DioxusRouterExt

* fix server function context

* fix fullstack desktop example

* fix axum auth example

* fix fullstack context type

* fix context providers path
This commit is contained in:
Evan Almloff 2024-06-19 03:38:06 +02:00 committed by GitHub
parent 7ee3dc41fc
commit b0caf442ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 152 additions and 64 deletions

8
Cargo.lock generated
View file

@ -203,12 +203,6 @@ version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "anymap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
[[package]]
name = "anymap2"
version = "0.13.0"
@ -2130,6 +2124,7 @@ dependencies = [
name = "dioxus"
version = "0.5.2"
dependencies = [
"axum",
"criterion 0.3.6",
"dioxus",
"dioxus-config-macro",
@ -2416,7 +2411,6 @@ dependencies = [
name = "dioxus-fullstack"
version = "0.5.2"
dependencies = [
"anymap",
"async-trait",
"axum",
"base64 0.21.7",

View file

@ -27,6 +27,7 @@ dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr ={ workspace = true, optional = true }
serde = { version = "1.0.136", optional = true }
axum = { workspace = true, optional = true }
[target.'cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")))'.dependencies]
dioxus-hot-reload = { workspace = true, optional = true }
@ -50,7 +51,7 @@ web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation
ssr = ["dep:dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
static-generation = ["dep:dioxus-static-site-generation", "dioxus-config-macro/static-generation"]
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum"]
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum", "dep:axum"]
# This feature just disables the no-renderer-enabled warning
third-party-renderer = []

View file

@ -31,7 +31,7 @@ type ValidContext = SendContext;
)))]
type ValidContext = UnsendContext;
type SendContext = dyn Fn() -> Box<dyn Any> + Send + Sync + 'static;
type SendContext = dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync + 'static;
type UnsendContext = dyn Fn() -> Box<dyn Any> + 'static;
@ -137,7 +137,7 @@ impl<Cfg> LaunchBuilder<Cfg, SendContext> {
/// Inject state into the root component's context that is created on the thread that the app is launched on.
pub fn with_context_provider(
mut self,
state: impl Fn() -> Box<dyn Any> + Send + Sync + 'static,
state: impl Fn() -> Box<dyn Any + Send + Sync> + Send + Sync + 'static,
) -> Self {
self.contexts.push(Box::new(state) as Box<SendContext>);
self

View file

@ -110,6 +110,10 @@ pub mod prelude {
#[cfg(feature = "router")]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
pub use dioxus_router::prelude::*;
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub use axum;
}
#[cfg(feature = "web")]

View file

@ -39,7 +39,6 @@ tracing = { workspace = true }
tracing-futures = { workspace = true, optional = true }
once_cell = "1.17.1"
tokio-util = { version = "0.7.8", features = ["rt"], optional = true }
anymap = { version = "0.12.1", optional = true }
serde = "1.0.159"
serde_json = { version = "1.0.95", optional = true }
@ -90,7 +89,6 @@ server = [
"dep:hyper",
"dep:http",
"dep:tower-layer",
"dep:anymap",
"dep:tracing-futures",
"dep:pin-project",
"dep:thiserror",

View file

@ -49,9 +49,7 @@ fn main() {
// build our application with some routes
let app = Router::new()
// Server side render the application, serve static assets, and register server functions
.serve_dioxus_application(ServeConfig::builder().build(), || {
VirtualDom::new(app)
})
.serve_dioxus_application(ServeConfig::default(), app)
.await
.layer(
axum_session_auth::AuthSessionLayer::<

View file

@ -19,7 +19,7 @@ async fn main() {
axum::serve(
listener,
axum::Router::new()
.register_server_fns()
.register_server_functions()
.into_make_service(),
)
.await

View file

@ -22,7 +22,7 @@
//! listener,
//! axum::Router::new()
//! // Server side render the application, serve static assets, and register server functions
//! .serve_dioxus_application("", ServerConfig::new(app, ()))
//! .serve_dioxus_application(ServerConfig::new(), app)
//! .into_make_service(),
//! )
//! .await
@ -60,12 +60,13 @@ use axum::{
http::{Request, Response, StatusCode},
response::IntoResponse,
};
use dioxus_lib::prelude::VirtualDom;
use dioxus_lib::prelude::{Element, VirtualDom};
use futures_util::Future;
use http::header::*;
use std::sync::Arc;
use crate::launch::ContextProviders;
use crate::prelude::*;
/// A extension trait with utilities for integrating Dioxus with your Axum router.
@ -74,9 +75,8 @@ pub trait DioxusRouterExt<S> {
///
/// # Example
/// ```rust
/// use dioxus_lib::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
@ -84,23 +84,49 @@ pub trait DioxusRouterExt<S> {
/// .serve(
/// axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_fns("")
/// .register_server_functions()
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn register_server_fns(self) -> Self;
fn register_server_functions(self) -> Self
where
Self: Sized,
{
self.register_server_functions_with_context(Default::default())
}
/// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler.
///
/// # Example
/// ```rust
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_functions_with_context(vec![Box::new(|| 1234567890u32)])
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_lib::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// # #![allow(non_snake_case)]
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
@ -133,10 +159,9 @@ pub trait DioxusRouterExt<S> {
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_lib::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// # #![allow(non_snake_case)]
/// # use dioxus_lib::prelude::*;
/// # use dioxus_fullstack::prelude::*;
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
@ -144,7 +169,7 @@ pub trait DioxusRouterExt<S> {
/// .serve(
/// axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_dioxus_application("", ServerConfig::new(app, ()))
/// .serve_dioxus_application(ServeConfig::new(), )
/// .into_make_service(),
/// )
/// .await
@ -158,7 +183,7 @@ pub trait DioxusRouterExt<S> {
fn serve_dioxus_application(
self,
cfg: impl Into<ServeConfig>,
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
app: fn() -> Element,
) -> impl Future<Output = Self> + Send + Sync
where
Self: Sized;
@ -168,12 +193,29 @@ impl<S> DioxusRouterExt<S> for Router<S>
where
S: Send + Sync + Clone + 'static,
{
fn register_server_fns(mut self) -> Self {
fn register_server_functions_with_context(
mut self,
context_providers: ContextProviders,
) -> Self {
use http::method::Method;
let context_providers = Arc::new(context_providers);
for (path, method) in server_fn::axum::server_fn_paths() {
tracing::trace!("Registering server function: {} {}", method, path);
let handler = move |req| handle_server_fns_inner(path, || {}, req);
let context_providers = context_providers.clone();
let handler = move |req| {
handle_server_fns_inner(
path,
move |server_context| {
for context_provider in context_providers.iter() {
let context = context_provider();
_ = server_context.insert_any(context);
}
},
req,
)
};
self = match method {
Method::GET => self.route(path, get(handler)),
Method::POST => self.route(path, post(handler)),
@ -234,7 +276,7 @@ where
fn serve_dioxus_application(
self,
cfg: impl Into<ServeConfig>,
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
app: fn() -> Element,
) -> impl Future<Output = Self> + Send + Sync {
let cfg = cfg.into();
async move {
@ -244,7 +286,7 @@ where
let mut server = self
.serve_static_assets(cfg.assets_path.clone())
.await
.register_server_fns();
.register_server_functions();
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
@ -254,7 +296,7 @@ where
server.fallback(get(render_handler).with_state((
cfg,
Arc::new(build_virtual_dom),
Arc::new(move || VirtualDom::new(app)),
ssr_state,
)))
}
@ -311,7 +353,7 @@ type AxumHandler<F> = (
/// .serve(
/// axum::Router::new()
/// // Register server functions, etc.
/// // Note you probably want to use `register_server_fns_with_handler`
/// // Note you can use `register_server_functions_with_context`
/// // to inject the context into server functions running outside
/// // of an SSR render context.
/// .fallback(get(render_handler_with_context).with_state((
@ -383,7 +425,7 @@ fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
/// A handler for Dioxus server functions. This will run the server function and return the result.
async fn handle_server_fns_inner(
path: &str,
additional_context: impl Fn() + 'static + Clone + Send,
additional_context: impl Fn(&DioxusServerContext) + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
use server_fn::middleware::Service;
@ -398,7 +440,7 @@ async fn handle_server_fns_inner(
server_fn::axum::get_server_fn_service(&path_string)
{
let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)));
additional_context();
additional_context(&server_context);
// store Accepts and Referrer in case we need them for redirect (below)
let accepts_html = req

View file

@ -116,9 +116,12 @@ impl Config {
pub async fn launch_server(
self,
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
context_providers: crate::launch::ContextProviders,
) {
use std::any::Any;
let addr = self.addr;
println!("Listening on {}", addr);
println!("Listening on http://{}", addr);
let cfg = self.server_cfg.build();
let server_fn_route = self.server_fn_route;
@ -129,7 +132,8 @@ impl Config {
use tower::ServiceBuilder;
let ssr_state = SSRState::new(&cfg);
let router = axum::Router::new().register_server_fns();
let router =
axum::Router::new().register_server_functions_with_context(context_providers);
#[cfg(not(any(feature = "desktop", feature = "mobile")))]
let router = {
let mut router = router.serve_static_assets(cfg.assets_path.clone()).await;

View file

@ -1,18 +1,21 @@
//! This module contains the `launch` function, which is the main entry point for dioxus fullstack
use std::any::Any;
use std::{any::Any, sync::Arc};
use dioxus_lib::prelude::{Element, VirtualDom};
pub use crate::Config;
pub(crate) type ContextProviders = Arc<
Vec<Box<dyn Fn() -> Box<dyn std::any::Any + Send + Sync + 'static> + Send + Sync + 'static>>,
>;
fn virtual_dom_factory(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
contexts: ContextProviders,
) -> impl Fn() -> VirtualDom + 'static {
move || {
let mut vdom = VirtualDom::new(root);
for context in &contexts {
for context in &*contexts {
vdom.insert_any_root_context(context());
}
vdom
@ -24,15 +27,16 @@ fn virtual_dom_factory(
#[allow(unused)]
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
platform_config: Config,
) -> ! {
let factory = virtual_dom_factory(root, contexts);
let contexts = Arc::new(contexts);
let factory = virtual_dom_factory(root, contexts.clone());
#[cfg(all(feature = "server", not(target_arch = "wasm32")))]
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
platform_config.launch_server(factory).await;
platform_config.launch_server(factory, contexts).await;
});
unreachable!("Launching a fullstack app should never return")
@ -43,7 +47,7 @@ pub fn launch(
#[allow(unused)]
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
platform_config: Config,
) {
let factory = virtual_dom_factory(root, contexts);

View file

@ -55,8 +55,8 @@ pub mod prelude {
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::server_context::{
extract, server_context, with_server_context, DioxusServerContext, FromServerContext,
ProvideServerContext,
extract, server_context, with_server_context, DioxusServerContext, FromContext,
FromServerContext, ProvideServerContext,
};
#[cfg(feature = "server")]

View file

@ -1,16 +1,19 @@
use crate::html_storage::HTMLData;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
type SendSyncAnyMap =
std::collections::HashMap<std::any::TypeId, Box<dyn Any + Send + Sync + 'static>>;
/// A shared context for server functions that contains information about the request and middleware state.
/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions.
///
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
#[derive(Clone)]
pub struct DioxusServerContext {
shared_context: std::sync::Arc<
std::sync::RwLock<anymap::Map<dyn anymap::any::Any + Send + Sync + 'static>>,
>,
shared_context: std::sync::Arc<std::sync::RwLock<SendSyncAnyMap>>,
response_parts: std::sync::Arc<std::sync::RwLock<http::response::Parts>>,
pub(crate) parts: Arc<tokio::sync::RwLock<http::request::Parts>>,
html_data: Arc<RwLock<HTMLData>>,
@ -20,7 +23,7 @@ pub struct DioxusServerContext {
impl Default for DioxusServerContext {
fn default() -> Self {
Self {
shared_context: std::sync::Arc::new(std::sync::RwLock::new(anymap::Map::new())),
shared_context: std::sync::Arc::new(std::sync::RwLock::new(HashMap::new())),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
@ -34,12 +37,10 @@ impl Default for DioxusServerContext {
mod server_fn_impl {
use super::*;
use std::any::{Any, TypeId};
use std::sync::LockResult;
use std::sync::{PoisonError, RwLockReadGuard, RwLockWriteGuard};
use anymap::{any::Any, Map};
type SendSyncAnyMap = Map<dyn Any + Send + Sync + 'static>;
impl DioxusServerContext {
/// Create a new server context from a request
pub fn new(parts: impl Into<Arc<tokio::sync::RwLock<http::request::Parts>>>) -> Self {
@ -55,17 +56,32 @@ mod server_fn_impl {
/// Clone a value from the shared server context
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
self.shared_context.read().ok()?.get::<T>().cloned()
self.shared_context
.read()
.ok()?
.get(&TypeId::of::<T>())
.map(|v| v.downcast_ref::<T>().unwrap().clone())
}
/// Insert a value into the shared server context
pub fn insert<T: Any + Send + Sync + 'static>(
&mut self,
&self,
value: T,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
self.shared_context
.write()
.map(|mut map| map.insert(value))
.map(|mut map| map.insert(TypeId::of::<T>(), Box::new(value)))
.map(|_| ())
}
/// Insert a Boxed `Any` value into the shared server context
pub fn insert_any(
&self,
value: Box<dyn Any + Send + Sync>,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
self.shared_context
.write()
.map(|mut map| map.insert((*value).type_id(), value))
.map(|_| ())
}
@ -232,14 +248,41 @@ impl<T: 'static> std::fmt::Display for NotFoundInServerContext<T> {
impl<T: 'static> std::error::Error for NotFoundInServerContext<T> {}
pub struct FromContext<T: std::marker::Send + std::marker::Sync + Clone + 'static>(pub(crate) T);
/// Extract a value from the server context provided through the launch builder context or [`DioxusServerContext::insert`]
///
/// Example:
/// ```rust, no_run
/// use dioxus::prelude::*;
///
/// LaunchBuilder::new()
/// // You can provide context to your whole app (including server functions) with the `with_context` method on the launch builder
/// .with_context(server_only! {
/// 1234567890u32
/// })
/// .launch(app);
///
/// #[server]
/// async fn read_context() -> Result<u32, ServerFnError> {
/// // You can extract values from the server context with the `extract` function
/// let FromContext(value) = extract().await?;
/// Ok(value)
/// }
///
/// fn app() -> Element {
/// let future = use_resource(read_context);
/// rsx! {
/// h1 { "{future:?}" }
/// }
/// }
/// ```
pub struct FromContext<T: std::marker::Send + std::marker::Sync + Clone + 'static>(pub T);
#[async_trait::async_trait]
impl<T: Send + Sync + Clone + 'static> FromServerContext for FromContext<T> {
type Rejection = NotFoundInServerContext<T>;
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
Ok(Self(req.clone().get::<T>().ok_or({
Ok(Self(req.get::<T>().ok_or({
NotFoundInServerContext::<T>(std::marker::PhantomData::<T>)
})?))
}