more fullstack cleanups: loaders serialize, ServeConfig only (#4785)
* some opinionated cleanups in dioxus-server * add serialize to loader * add `result` to Resource * clippy * fix * writable memos, resources, `onmount`, `.pending`, and `.value()` on actions * fix compile issues * fix typo
This commit is contained in:
1 parent
a3e40a5ea2
commit
7102bc3b6a
33 files changed
+2067
-654
No files matched your search
@@ -43,11 +43,11 @@ fn app() -> Element {
|
|||||||
h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" }
|
h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" }
|
||||||
button {
|
button {
|
||||||
class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200",
|
class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200",
|
||||||
disabled: scan.is_pending(),
|
disabled: scan.pending(),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
scan.call();
|
scan.call();
|
||||||
},
|
},
|
||||||
if scan.is_pending() { "Scanning" } else { "Scan" }
|
if scan.pending() { "Scanning" } else { "Scan" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +64,8 @@ fn app() -> Element {
|
|||||||
th { class: "pb-3 px-2 font-medium", "Security" }
|
th { class: "pb-3 px-2 font-medium", "Security" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match scan.result() {
|
match scan.value() {
|
||||||
None if scan.is_pending() => rsx! { "Scanning..." },
|
None if scan.pending() => rsx! { "Scanning..." },
|
||||||
None => rsx! { "Press Scan to start scanning" },
|
None => rsx! { "Press Scan to start scanning" },
|
||||||
Some(Err(_err)) => rsx! { "Failed to scan" },
|
Some(Err(_err)) => rsx! { "Failed to scan" },
|
||||||
Some(Ok(peripherals)) => rsx! {
|
Some(Ok(peripherals)) => rsx! {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ fn app() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
match breed.result() {
|
match breed.value() {
|
||||||
None => rsx! { div { "Click the button to fetch a dog!" } },
|
None => rsx! { div { "Click the button to fetch a dog!" } },
|
||||||
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
|
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
|
||||||
Some(Ok(res)) => rsx! {
|
Some(Ok(res)) => rsx! {
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ fn app() -> Element {
|
|||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "button is-primary",
|
class: "button is-primary",
|
||||||
class: if image.is_pending() { "is-loading" },
|
class: if image.pending() { "is-loading" },
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
image.call();
|
image.call();
|
||||||
},
|
},
|
||||||
"Generate image"
|
"Generate image"
|
||||||
}
|
}
|
||||||
if let Some(image) = image.value() {
|
if let Some(Ok(image)) = image.value() {
|
||||||
for image in image.read().data.as_slice() {
|
for image in image.read().data.as_slice() {
|
||||||
section { class: "is-flex",
|
section { class: "is-flex",
|
||||||
div { class: "container is-fluid",
|
div { class: "container is-fluid",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fn app() -> Element {
|
|||||||
let current_weather = use_loader(move || get_weather(country()));
|
let current_weather = use_loader(move || get_weather(country()));
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
Stylesheet { href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }
|
Stylesheet { href: asset!("/examples/assets/weatherapp.css") }
|
||||||
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
|
div { class: "mx-auto p-4 bg-gray-100 h-screen flex justify-center",
|
||||||
div { class: "flex items-center justify-center flex-row",
|
div { class: "flex items-center justify-center flex-row",
|
||||||
div { class: "flex items-start justify-center flex-row",
|
div { class: "flex items-start justify-center flex-row",
|
||||||
@@ -35,6 +35,11 @@ fn app() -> Element {
|
|||||||
weather: weather.cloned(),
|
weather: weather.cloned(),
|
||||||
}
|
}
|
||||||
Forecast { weather: weather.cloned() }
|
Forecast { weather: weather.cloned() }
|
||||||
|
div { height: "20px", margin_top: "10px",
|
||||||
|
if weather.loading() {
|
||||||
|
"Fetching weather data..."
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(Loading::Pending(_)) => rsx! {
|
Err(Loading::Pending(_)) => rsx! {
|
||||||
div { "Loading weather data..." }
|
div { "Loading weather data..." }
|
||||||
@@ -56,8 +61,8 @@ fn app() -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
fn CountryData(weather: WeatherResponse, country: WeatherLocation) -> Element {
|
fn CountryData(weather: WeatherResponse, country: WeatherLocation) -> Element {
|
||||||
let today = "Today";
|
let today = "Today";
|
||||||
let max_temp = weather.daily.temperature_2m_max.first().unwrap();
|
let max_temp = weather.daily.temperature_2m_max[0];
|
||||||
let min_temp = weather.daily.temperature_2m_min.first().unwrap();
|
let min_temp = weather.daily.temperature_2m_min[0];
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "flex mb-4 justify-between items-center",
|
div { class: "flex mb-4 justify-between items-center",
|
||||||
@@ -82,15 +87,11 @@ fn CountryData(weather: WeatherResponse, country: WeatherLocation) -> Element {
|
|||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[component]
|
#[component]
|
||||||
fn Forecast(weather: WeatherResponse) -> Element {
|
fn Forecast(weather: WeatherResponse) -> Element {
|
||||||
let today = (weather.daily.temperature_2m_max.first().unwrap()
|
let today = (weather.daily.temperature_2m_max[0] + weather.daily.temperature_2m_max[0]) / 2.0;
|
||||||
+ weather.daily.temperature_2m_max.first().unwrap())
|
let tomorrow =
|
||||||
/ 2.0;
|
(weather.daily.temperature_2m_max[1] + weather.daily.temperature_2m_max[1]) / 2.0;
|
||||||
let tomorrow = (weather.daily.temperature_2m_max.get(1).unwrap()
|
let past_tomorrow =
|
||||||
+ weather.daily.temperature_2m_max.get(1).unwrap())
|
(weather.daily.temperature_2m_max[2] + weather.daily.temperature_2m_max[2]) / 2.0;
|
||||||
/ 2.0;
|
|
||||||
let past_tomorrow = (weather.daily.temperature_2m_max.get(2).unwrap()
|
|
||||||
+ weather.daily.temperature_2m_max.get(2).unwrap())
|
|
||||||
/ 2.0;
|
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "px-6 pt-4 relative",
|
div { class: "px-6 pt-4 relative",
|
||||||
@@ -117,7 +118,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
|
fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
|
||||||
let mut input = use_signal(|| "".to_string());
|
let mut input = use_signal(|| "".to_string());
|
||||||
let locations = use_resource(move || get_locations(input()));
|
let locations = use_loader(move || get_locations(input()));
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
@@ -145,8 +146,14 @@ fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul { class: "bg-white border border-gray-100 w-full mt-2 max-h-72 overflow-auto",
|
ul { class: "bg-white border border-gray-100 w-full mt-2 max-h-72 overflow-auto",
|
||||||
if let Some(Ok(locs)) = locations.read().as_ref() {
|
match locations {
|
||||||
for wl in locs.iter().take(5).cloned() {
|
Ok(locs) if locs.is_empty() => rsx! {
|
||||||
|
li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative",
|
||||||
|
"No locations found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(locs) => rsx! {
|
||||||
|
for wl in locs.read().iter().take(5).cloned() {
|
||||||
li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900",
|
li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900",
|
||||||
onclick: move |_| country.set(wl.clone()),
|
onclick: move |_| country.set(wl.clone()),
|
||||||
MapIcon {}
|
MapIcon {}
|
||||||
@@ -154,8 +161,17 @@ fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
|
|||||||
" · {wl.country}"
|
" · {wl.country}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
"loading locations..."
|
Err(Loading::Pending(_)) => rsx! {
|
||||||
|
li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative",
|
||||||
|
"Searching..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(Loading::Failed(handle)) => rsx! {
|
||||||
|
li { class: "pl-8 pr-2 py-1 border-b-2 border-gray-100 relative",
|
||||||
|
"Failed to search: {handle.error():?}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,10 +216,11 @@ type WeatherLocations = Vec<WeatherLocation>;
|
|||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
struct SearchResponse {
|
struct SearchResponse {
|
||||||
|
#[serde(default)]
|
||||||
results: WeatherLocations,
|
results: WeatherLocations,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_locations(input: impl Display) -> reqwest::Result<WeatherLocations> {
|
async fn get_locations(input: impl Display) -> Result<WeatherLocations> {
|
||||||
let res = reqwest::get(&format!(
|
let res = reqwest::get(&format!(
|
||||||
"https://geocoding-api.open-meteo.com/v1/search?name={input}"
|
"https://geocoding-api.open-meteo.com/v1/search?name={input}"
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ fn Doggo() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reqwest::get("https://dog.ceo/api/breeds/image/random/")
|
reqwest::get("https://dog.ceo/api/breeds/image/random/")
|
||||||
.await
|
.await?
|
||||||
.unwrap()
|
|
||||||
.json::<DogApi>()
|
.json::<DogApi>()
|
||||||
.await
|
.await
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ fn app() -> Element {
|
|||||||
"Fetch User Info"
|
"Fetch User Info"
|
||||||
}
|
}
|
||||||
|
|
||||||
pre { "Logged in: {login.result():?}" }
|
pre { "Logged in: {login.value():?}" }
|
||||||
pre { "User name: {user_name.result():?}" }
|
pre { "User name: {user_name.value():?}" }
|
||||||
pre { "Permissions: {permissions.result():?}" }
|
pre { "Permissions: {permissions.value():?}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ fn app() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
match breed.result() {
|
match breed.value() {
|
||||||
None => rsx! { div { "Click the button to fetch a dog!" } },
|
None => rsx! { div { "Click the button to fetch a dog!" } },
|
||||||
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
|
Some(Err(_e)) => rsx! { div { "Failed to fetch a dog, please try again." } },
|
||||||
Some(Ok(res)) => rsx! { img { max_width: "500px", max_height: "500px", src: "{res}" } },
|
Some(Ok(res)) => rsx! { img { max_width: "500px", max_height: "500px", src: "{res}" } },
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ fn main() {
|
|||||||
pre { "Dog data: {dog_data.value():#?}" }
|
pre { "Dog data: {dog_data.value():#?}" }
|
||||||
pre { "IP data: {ip_data.value():#?}" }
|
pre { "IP data: {ip_data.value():#?}" }
|
||||||
pre { "Custom encoded data: {custom_data.value():#?}" }
|
pre { "Custom encoded data: {custom_data.value():#?}" }
|
||||||
pre { "Error data: {error_data.result():#?}" }
|
pre { "Error data: {error_data.value():#?}" }
|
||||||
pre { "Typed error data: {typed_error_data.result():#?}" }
|
pre { "Typed error data: {typed_error_data.value():#?}" }
|
||||||
pre { "HTTP 400 data: {http_error_data.result():#?}" }
|
pre { "HTTP 400 data: {http_error_data.value():#?}" }
|
||||||
pre { "HTTP 400 (context) data: {http_error_context_data.result():#?}" }
|
pre { "HTTP 400 (context) data: {http_error_context_data.value():#?}" }
|
||||||
pre { "Dog error data: {dog_data_err.result():#?}" }
|
pre { "Dog error data: {dog_data_err.value():#?}" }
|
||||||
pre { "Throws ok data: {throws_ok_data.result():#?}" }
|
pre { "Throws ok data: {throws_ok_data.value():#?}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ fn app() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
h1 { "Header Map Example" }
|
h1 { "Header Map Example" }
|
||||||
button { onclick: move |_| headers.call(), "Get Headers" }
|
button { onclick: move |_| headers.call(), "Get Headers" }
|
||||||
if let Some(Ok(headers)) = headers.result() {
|
if let Some(Ok(headers)) = headers.value() {
|
||||||
p { "Response from server:" }
|
p { "Response from server:" }
|
||||||
pre { "{headers}" }
|
pre { "{headers}" }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ fn app() -> Element {
|
|||||||
div { style: "margin: 1rem 0;",
|
div { style: "margin: 1rem 0;",
|
||||||
h2 { "Server Greeting" }
|
h2 { "Server Greeting" }
|
||||||
button { onclick: move |_| message.call("World".to_string(), 30), "Get Server Greeting" }
|
button { onclick: move |_| message.call("World".to_string(), 30), "Get Server Greeting" }
|
||||||
if message.is_pending() {
|
if message.pending() {
|
||||||
p { "Loading..." }
|
p { "Loading..." }
|
||||||
}
|
}
|
||||||
p { "{message:#?}" }
|
p { "{message:#?}" }
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fn app() -> Element {
|
|||||||
},
|
},
|
||||||
"Get Sensitive Data",
|
"Get Sensitive Data",
|
||||||
}
|
}
|
||||||
pre { "Response from locked API: {fetch_sensitive.result():?}"}
|
pre { "Response from locked API: {fetch_sensitive.value():?}"}
|
||||||
form {
|
form {
|
||||||
onsubmit: move |evt: FormEvent| async move {
|
onsubmit: move |evt: FormEvent| async move {
|
||||||
// Prevent the browser from navigating away.
|
// Prevent the browser from navigating away.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ fn app() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
h1 { "Fullstack Middleware Example" }
|
h1 { "Fullstack Middleware Example" }
|
||||||
button { onclick: move |_| per_route.call(), "Fetch Data" }
|
button { onclick: move |_| per_route.call(), "Fetch Data" }
|
||||||
pre { "{per_route.result():#?}" }
|
pre { "{per_route.value():#?}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,13 +174,13 @@ fn app() -> Element {
|
|||||||
"Clear results"
|
"Clear results"
|
||||||
}
|
}
|
||||||
|
|
||||||
pre { "Echo result: {echo_action.result():#?}" }
|
pre { "Echo result: {echo_action.value():#?}" }
|
||||||
pre { "Chat result: {chat_action.result():#?}" }
|
pre { "Chat result: {chat_action.value():#?}" }
|
||||||
pre { "Dog data: {dog_data.result():#?}" }
|
pre { "Dog data: {dog_data.value():#?}" }
|
||||||
pre { "Custom data: {custom_data.result():#?}" }
|
pre { "Custom data: {custom_data.value():#?}" }
|
||||||
pre { "Anonymous: {anonymous_action.result():#?}" }
|
pre { "Anonymous: {anonymous_action.value():#?}" }
|
||||||
pre { "Custom anonymous: {custom_anonymous_action.result():#?}" }
|
pre { "Custom anonymous: {custom_anonymous_action.value():#?}" }
|
||||||
pre { "Custom response: {custom_response_action.result():#?}" }
|
pre { "Custom response: {custom_response_action.value():#?}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,13 +108,13 @@ fn app() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
button { onclick: move |_| users.call(), "Get Users" }
|
button { onclick: move |_| users.call(), "Get Users" }
|
||||||
pre { "{users.result():?}" }
|
pre { "{users.value():?}" }
|
||||||
button { onclick: move |_| messages.call(), "Get Messages" }
|
button { onclick: move |_| messages.call(), "Get Messages" }
|
||||||
pre { "{messages.result():?}" }
|
pre { "{messages.value():?}" }
|
||||||
button { onclick: move |_| broadcast.call(), "Broadcast Message" }
|
button { onclick: move |_| broadcast.call(), "Broadcast Message" }
|
||||||
pre { "{broadcast.result():?}" }
|
pre { "{broadcast.value():?}" }
|
||||||
button { onclick: move |_| add.call(), "Add Message" }
|
button { onclick: move |_| add.call(), "Add Message" }
|
||||||
pre { "{add.result():?}" }
|
pre { "{add.value():?}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,12 +36,12 @@ fn Home() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn Post(id: u32) -> Element {
|
fn Post(id: ReadSignal<u32>) -> Element {
|
||||||
// You can return `HttpError` to return a specific HTTP status code and message.
|
// You can return `HttpError` to return a specific HTTP status code and message.
|
||||||
// `404 Not Found` will cause the server to return a 404 status code.
|
// `404 Not Found` will cause the server to return a 404 status code.
|
||||||
//
|
//
|
||||||
// `use_loader` will suspend the server-side rendering until the future resolves.
|
// `use_loader` will suspend the server-side rendering until the future resolves.
|
||||||
let post_data = use_loader(move || get_post(id))?;
|
let post_data = use_loader(move || get_post(id()))?;
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "Post {id}" }
|
h1 { "Post {id}" }
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ fn app() -> Element {
|
|||||||
},
|
},
|
||||||
"Drop files here"
|
"Drop files here"
|
||||||
}
|
}
|
||||||
pre { "{upload_as_file_upload.result():?}" }
|
pre { "{upload_as_file_upload.value():?}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -99,9 +99,9 @@ fn app() -> Element {
|
|||||||
div {
|
div {
|
||||||
h3 { "Download a file from the server" }
|
h3 { "Download a file from the server" }
|
||||||
button { onclick: move |_| download_file.call(), "Download file" }
|
button { onclick: move |_| download_file.call(), "Download file" }
|
||||||
if let Some(Ok(content)) = &download_file.result() {
|
if let Some(Ok(content)) = &download_file.value() {
|
||||||
pre { "{content}" }
|
pre { "{content}" }
|
||||||
} else if let Some(Err(e)) = &download_file.result() {
|
} else if let Some(Err(e)) = &download_file.value() {
|
||||||
pre { "Error downloading file: {e}" }
|
pre { "Error downloading file: {e}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,17 +1,14 @@
|
|||||||
use dioxus_core::{
|
use dioxus_core::{use_hook, IntoAttributeValue, IntoDynNode, Subscribers};
|
||||||
spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, Subscribers, Task,
|
|
||||||
};
|
|
||||||
use dioxus_core::{CapturedError, RenderError, Result, SuspendedFuture};
|
use dioxus_core::{CapturedError, RenderError, Result, SuspendedFuture};
|
||||||
use dioxus_hooks::{use_callback, use_signal};
|
use dioxus_hooks::{use_resource, use_signal, Resource};
|
||||||
use dioxus_signals::{
|
use dioxus_signals::{
|
||||||
read_impls, ReadSignal, Readable, ReadableBoxExt, ReadableExt, ReadableRef, Signal, Writable,
|
read_impls, ReadSignal, Readable, ReadableBoxExt, ReadableExt, ReadableRef, Signal, Writable,
|
||||||
WritableExt, WriteLock,
|
WritableExt, WriteLock,
|
||||||
};
|
};
|
||||||
use futures_util::{future, pin_mut, FutureExt, StreamExt};
|
|
||||||
use generational_box::{BorrowResult, UnsyncStorage};
|
use generational_box::{BorrowResult, UnsyncStorage};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::future::Future;
|
use std::ops::Deref;
|
||||||
use std::{cell::Cell, ops::Deref, rc::Rc};
|
use std::{cmp::PartialEq, future::Future};
|
||||||
|
|
||||||
/// A hook to create a resource that loads data asynchronously.
|
/// A hook to create a resource that loads data asynchronously.
|
||||||
///
|
///
|
||||||
@@ -35,105 +32,118 @@ use std::{cell::Cell, ops::Deref, rc::Rc};
|
|||||||
pub fn use_loader<F, T, E>(mut future: impl FnMut() -> F + 'static) -> Result<Loader<T>, Loading>
|
pub fn use_loader<F, T, E>(mut future: impl FnMut() -> F + 'static) -> Result<Loader<T>, Loading>
|
||||||
where
|
where
|
||||||
F: Future<Output = Result<T, E>> + 'static,
|
F: Future<Output = Result<T, E>> + 'static,
|
||||||
T: 'static + std::cmp::PartialEq + Serialize + DeserializeOwned,
|
T: 'static + PartialEq + Serialize + DeserializeOwned,
|
||||||
E: Into<dioxus_core::Error> + 'static,
|
E: Into<dioxus_core::Error> + 'static,
|
||||||
{
|
{
|
||||||
let location = std::panic::Location::caller();
|
let serialize_context = use_hook(crate::transport::serialize_context);
|
||||||
|
|
||||||
// todo: wire up serialize context into loader
|
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||||
// let serialize_context = use_hook(crate::transport::serialize_context);
|
#[allow(unused)]
|
||||||
// // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
let storage_entry: crate::transport::SerializeContextEntry<Result<T, CapturedError>> =
|
||||||
// let storage_entry: SerializeContextEntry<T> = use_hook(|| serialize_context.create_entry());
|
use_hook(|| serialize_context.create_entry());
|
||||||
// use crate::{transport::SerializeContextEntry, use_server_future, use_server_future_unsuspended};
|
|
||||||
|
|
||||||
let mut err = use_signal(|| None as Option<CapturedError>);
|
#[cfg(feature = "server")]
|
||||||
|
let caller = std::panic::Location::caller();
|
||||||
|
|
||||||
|
// If this is the first run and we are on the web client, the data might be cached
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
let initial_web_result =
|
||||||
|
use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
||||||
|
|
||||||
|
let mut error = use_signal(|| None as Option<CapturedError>);
|
||||||
let mut value = use_signal(|| None as Option<T>);
|
let mut value = use_signal(|| None as Option<T>);
|
||||||
let mut state = use_signal(|| LoaderState::Pending);
|
let mut loader_state = use_signal(|| LoaderState::Pending);
|
||||||
let (rc, changed) = use_hook(|| {
|
|
||||||
let (rc, changed) = ReactiveContext::new_with_origin(location);
|
|
||||||
(rc, Rc::new(Cell::new(Some(changed))))
|
|
||||||
});
|
|
||||||
|
|
||||||
let callback = use_callback(move |_| {
|
let resource = use_resource(move || {
|
||||||
// Set the state to Pending when the task is restarted
|
#[cfg(feature = "server")]
|
||||||
state.set(LoaderState::Pending);
|
let storage_entry = storage_entry.clone();
|
||||||
|
|
||||||
// Create the user's task
|
let user_fut = future();
|
||||||
let fut = rc.reset_and_run_in(&mut future);
|
|
||||||
|
|
||||||
// Spawn a wrapper task that polls the inner future and watches its dependencies
|
#[cfg(feature = "web")]
|
||||||
spawn(async move {
|
let initial_web_result = initial_web_result.clone();
|
||||||
// Move the future here and pin it so we can poll it
|
|
||||||
let fut = fut;
|
|
||||||
pin_mut!(fut);
|
|
||||||
|
|
||||||
// Run each poll in the context of the reactive scope
|
#[allow(clippy::let_and_return)]
|
||||||
// This ensures the scope is properly subscribed to the future's dependencies
|
async move {
|
||||||
let res = future::poll_fn(|cx| {
|
// If this is the first run and we are on the web client, the data might be cached
|
||||||
rc.run_in(|| {
|
#[cfg(feature = "web")]
|
||||||
tracing::trace_span!("polling resource", location = %location)
|
match initial_web_result.take() {
|
||||||
.in_scope(|| fut.poll_unpin(cx))
|
// The data was deserialized successfully from the server
|
||||||
})
|
Some(Ok(o)) => {
|
||||||
})
|
match o {
|
||||||
.await;
|
|
||||||
|
|
||||||
// Map the error to the captured error type so it's cheap to clone and pass out
|
|
||||||
// We go through the anyhow::Error first since it's more useful.
|
|
||||||
let res: Result<T, CapturedError> = res.map_err(|e| {
|
|
||||||
let res: dioxus_core::Error = e.into();
|
|
||||||
res.into()
|
|
||||||
});
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
err.set(None);
|
|
||||||
value.set(Some(v));
|
value.set(Some(v));
|
||||||
state.set(LoaderState::Ready);
|
loader_state.set(LoaderState::Ready);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
err.set(Some(e));
|
error.set(Some(e));
|
||||||
state.set(LoaderState::Failed);
|
loader_state.set(LoaderState::Failed);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
// The data is still pending from the server. Don't try to resolve it on the client
|
||||||
|
Some(Err(crate::transport::TakeDataError::DataPending)) => {
|
||||||
|
std::future::pending::<()>().await
|
||||||
|
}
|
||||||
|
|
||||||
|
// The data was not available on the server, rerun the future
|
||||||
|
Some(Err(_)) => {}
|
||||||
|
|
||||||
|
// This isn't the first run, so we don't need do anything
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just run the future itself
|
||||||
|
let out = user_fut.await;
|
||||||
|
|
||||||
|
// Remap the error to the captured error type so it's cheap to clone and pass out, just
|
||||||
|
// slightly more cumbersome to access the inner error.
|
||||||
|
let out = out.map_err(|e| {
|
||||||
|
let anyhow_err: anyhow::Error = e.into();
|
||||||
|
anyhow_err.into()
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut task = use_hook(|| Signal::new(callback(())));
|
// If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
storage_entry.insert(&out, caller);
|
||||||
|
|
||||||
use_hook(|| {
|
match out {
|
||||||
let mut changed = changed.take().unwrap();
|
Ok(v) => {
|
||||||
spawn(async move {
|
value.set(Some(v));
|
||||||
loop {
|
loader_state.set(LoaderState::Ready);
|
||||||
// Wait for the dependencies to change
|
|
||||||
let _ = changed.next().await;
|
|
||||||
|
|
||||||
// Stop the old task
|
|
||||||
task.write().cancel();
|
|
||||||
|
|
||||||
// Start a new task
|
|
||||||
task.set(callback(()));
|
|
||||||
}
|
}
|
||||||
})
|
Err(e) => {
|
||||||
|
error.set(Some(e));
|
||||||
|
loader_state.set(LoaderState::Failed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// On the first run, force this task to be polled right away in case its value is ready
|
||||||
|
use_hook(|| {
|
||||||
|
let _ = resource.task().poll_now();
|
||||||
});
|
});
|
||||||
|
|
||||||
let read_value = use_hook(|| value.map(|f| f.as_ref().unwrap()).boxed());
|
let read_value = use_hook(|| value.map(|f| f.as_ref().unwrap()).boxed());
|
||||||
|
|
||||||
let handle = LoaderHandle {
|
let handle = LoaderHandle {
|
||||||
task,
|
resource,
|
||||||
err,
|
error,
|
||||||
callback,
|
state: loader_state,
|
||||||
state,
|
_marker: std::marker::PhantomData,
|
||||||
};
|
};
|
||||||
|
|
||||||
match &*state.read_unchecked() {
|
match &*loader_state.read_unchecked() {
|
||||||
LoaderState::Pending => Err(Loading::Pending(handle)),
|
LoaderState::Pending => Err(Loading::Pending(handle)),
|
||||||
LoaderState::Failed => Err(Loading::Failed(handle)),
|
LoaderState::Failed => Err(Loading::Failed(handle)),
|
||||||
LoaderState::Ready => Ok(Loader {
|
LoaderState::Ready => Ok(Loader {
|
||||||
real_value: value,
|
real_value: value,
|
||||||
read_value,
|
read_value,
|
||||||
error: err,
|
error,
|
||||||
state,
|
state: loader_state,
|
||||||
task,
|
|
||||||
handle,
|
handle,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -153,7 +163,6 @@ pub struct Loader<T: 'static> {
|
|||||||
real_value: Signal<Option<T>>,
|
real_value: Signal<Option<T>>,
|
||||||
error: Signal<Option<CapturedError>>,
|
error: Signal<Option<CapturedError>>,
|
||||||
state: Signal<LoaderState>,
|
state: Signal<LoaderState>,
|
||||||
task: Signal<Task>,
|
|
||||||
handle: LoaderHandle,
|
handle: LoaderHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +187,12 @@ impl<T: 'static> Loader<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel the current loading task.
|
/// Cancel the current loading task.
|
||||||
pub fn cancel(&self) {
|
pub fn cancel(&mut self) {
|
||||||
self.task.read().cancel();
|
self.handle.resource.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loading(&self) -> bool {
|
||||||
|
!self.handle.resource.finished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,25 +307,27 @@ pub enum LoaderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct LoaderHandle {
|
pub struct LoaderHandle<M = ()> {
|
||||||
callback: Callback<(), Task>,
|
resource: Resource<()>,
|
||||||
task: Signal<Task>,
|
error: Signal<Option<CapturedError>>,
|
||||||
err: Signal<Option<CapturedError>>,
|
|
||||||
state: Signal<LoaderState>,
|
state: Signal<LoaderState>,
|
||||||
|
_marker: std::marker::PhantomData<M>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoaderHandle {
|
impl LoaderHandle {
|
||||||
/// Restart the loading task.
|
/// Restart the loading task.
|
||||||
pub fn restart(&mut self) {
|
pub fn restart(&mut self) {
|
||||||
self.task.write().cancel();
|
self.resource.restart();
|
||||||
let new_task = self.callback.call(());
|
|
||||||
self.task.set(new_task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current state of the loader.
|
/// Get the current state of the loader.
|
||||||
pub fn state(&self) -> LoaderState {
|
pub fn state(&self) -> LoaderState {
|
||||||
*self.state.read()
|
*self.state.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn error(&self) -> Option<CapturedError> {
|
||||||
|
self.error.read().as_ref().cloned()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for LoaderHandle {
|
impl Clone for LoaderHandle {
|
||||||
@@ -354,9 +369,9 @@ impl std::fmt::Display for Loading {
|
|||||||
impl From<Loading> for RenderError {
|
impl From<Loading> for RenderError {
|
||||||
fn from(val: Loading) -> Self {
|
fn from(val: Loading) -> Self {
|
||||||
match val {
|
match val {
|
||||||
Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.task.cloned())),
|
Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.resource.task())),
|
||||||
Loading::Failed(err) => RenderError::Error(
|
Loading::Failed(err) => RenderError::Error(
|
||||||
err.err
|
err.error
|
||||||
.cloned()
|
.cloned()
|
||||||
.expect("LoaderHandle in Failed state should always have an error"),
|
.expect("LoaderHandle in Failed state should always have an error"),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ async fn main() {
|
|||||||
let router = axum::Router::new()
|
let router = axum::Router::new()
|
||||||
// You can add a dioxus application to the router with the `serve_dioxus_application` method
|
// You can add a dioxus application to the router with the `serve_dioxus_application` method
|
||||||
// This will add a fallback route to the router that will serve your component and server functions
|
// This will add a fallback route to the router that will serve your component and server functions
|
||||||
.serve_dioxus_application(dioxus_server::ServeConfig::new().unwrap(), App);
|
.serve_dioxus_application(dioxus_server::ServeConfig::new(), App);
|
||||||
|
|
||||||
// Finally, we can launch the server
|
// Finally, we can launch the server
|
||||||
let router = router.into_make_service();
|
let router = router.into_make_service();
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
This method interacts with information from the current request. The request may come from:
|
|
||||||
|
|
||||||
1. The initial SSR render if this method called from a [`Component`](dioxus_core_macro::component) or a [`server`](dioxus_server) function that is called during the initial render
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# use dioxus::prelude::*;
|
|
||||||
#[component]
|
|
||||||
fn PrintHtmlRequestInfo() -> Element {
|
|
||||||
// The server context only exists on the server, so we need to put it behind a server_only! config
|
|
||||||
server_only! {
|
|
||||||
// Since we are calling this from a component, the server context that is returned will be from
|
|
||||||
// the html request for ssr rendering
|
|
||||||
let context = server_context();
|
|
||||||
let request_parts = context.request_parts();
|
|
||||||
println!("headers are {:?}", request_parts.headers);
|
|
||||||
}
|
|
||||||
rsx! {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. A request to a [`server`](dioxus_server) function called directly from the client (either on desktop/mobile or on the web frontend after the initial render)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# use dioxus::prelude::*;
|
|
||||||
#[server]
|
|
||||||
async fn read_headers() -> ServerFnResult {
|
|
||||||
// Since we are calling this from a server function, the server context that is may be from the
|
|
||||||
// initial request or a request from the client
|
|
||||||
let context = server_context();
|
|
||||||
let request_parts = context.request_parts();
|
|
||||||
println!("headers are {:?}", request_parts.headers);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn CallServerFunction() -> Element {
|
|
||||||
rsx! {
|
|
||||||
button {
|
|
||||||
// If you click the button, the server function will be called and the server context will be
|
|
||||||
// from the client request
|
|
||||||
onclick: move |_| async {
|
|
||||||
_ = read_headers().await
|
|
||||||
},
|
|
||||||
"Call server function"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -3,37 +3,51 @@
|
|||||||
|
|
||||||
use dioxus_core::LaunchConfig;
|
use dioxus_core::LaunchConfig;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::IncrementalRendererConfig;
|
use crate::{IncrementalRendererConfig, IndexHtml};
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(crate) type ContextProviders = Arc<Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>>;
|
pub(crate) type ContextProviders = Arc<Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>>;
|
||||||
|
|
||||||
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`].
|
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`].
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ServeConfigBuilder {
|
pub struct ServeConfig {
|
||||||
pub(crate) root_id: Option<&'static str>,
|
pub(crate) root_id: Option<&'static str>,
|
||||||
pub(crate) index_html: Option<String>,
|
pub(crate) index: IndexHtml,
|
||||||
pub(crate) index_path: Option<PathBuf>,
|
pub(crate) index_html_override: Option<String>,
|
||||||
pub(crate) incremental: Option<IncrementalRendererConfig>,
|
pub(crate) incremental: Option<IncrementalRendererConfig>,
|
||||||
pub(crate) context_providers: Vec<Arc<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
|
pub(crate) context_providers: Vec<Arc<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
|
||||||
pub(crate) streaming_mode: StreamingMode,
|
pub(crate) streaming_mode: StreamingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LaunchConfig for ServeConfigBuilder {}
|
/// The streaming mode to use while rendering the page
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq)]
|
||||||
|
pub enum StreamingMode {
|
||||||
|
/// Streaming is disabled; all server futures should be resolved before hydrating the page on the client
|
||||||
|
#[default]
|
||||||
|
Disabled,
|
||||||
|
|
||||||
impl ServeConfigBuilder {
|
/// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client
|
||||||
/// Create a new ServeConfigBuilder with incremental static generation disabled and the default index.html settings
|
/// as they resolve
|
||||||
|
OutOfOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LaunchConfig for ServeConfig {}
|
||||||
|
|
||||||
|
impl ServeConfig {
|
||||||
|
/// Create a new ServeConfig with incremental static generation disabled and the default index.html settings.
|
||||||
|
pub fn builder() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new ServeConfig with incremental static generation disabled and the default index.html settings
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
root_id: None,
|
root_id: None,
|
||||||
index_html: None,
|
|
||||||
index_path: None,
|
|
||||||
incremental: None,
|
incremental: None,
|
||||||
|
index: IndexHtml::default(),
|
||||||
|
index_html_override: None,
|
||||||
context_providers: Default::default(),
|
context_providers: Default::default(),
|
||||||
streaming_mode: StreamingMode::default(),
|
streaming_mode: StreamingMode::default(),
|
||||||
}
|
}
|
||||||
@@ -49,7 +63,7 @@ impl ServeConfigBuilder {
|
|||||||
/// // Finally, launch the app with the config
|
/// // Finally, launch the app with the config
|
||||||
/// LaunchBuilder::new()
|
/// LaunchBuilder::new()
|
||||||
/// // Only set the server config if the server feature is enabled
|
/// // Only set the server config if the server feature is enabled
|
||||||
/// .with_cfg(server_only!(dioxus_server::ServeConfigBuilder::default().incremental(dioxus_server::IncrementalRendererConfig::default())))
|
/// .with_cfg(server_only!(dioxus_server::ServeConfig::default().incremental(dioxus_server::IncrementalRendererConfig::default())))
|
||||||
/// .launch(app);
|
/// .launch(app);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn incremental(mut self, cfg: IncrementalRendererConfig) -> Self {
|
pub fn incremental(mut self, cfg: IncrementalRendererConfig) -> Self {
|
||||||
@@ -57,15 +71,9 @@ impl ServeConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the contents of the index.html file to be served. (precedence over index_path)
|
/// Set the contents of the index.html file to be served.
|
||||||
pub fn index_html(mut self, index_html: String) -> Self {
|
pub fn index_html(mut self, index_html: String) -> Self {
|
||||||
self.index_html = Some(index_html);
|
self.index_html_override = Some(index_html);
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
|
|
||||||
pub fn index_path(mut self, index_path: PathBuf) -> Self {
|
|
||||||
self.index_path = Some(index_path);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +104,7 @@ impl ServeConfigBuilder {
|
|||||||
/// LaunchBuilder::new()
|
/// LaunchBuilder::new()
|
||||||
/// // Only set the server config if the server feature is enabled
|
/// // Only set the server config if the server feature is enabled
|
||||||
/// .with_cfg(server_only! {
|
/// .with_cfg(server_only! {
|
||||||
/// dioxus_server::ServeConfigBuilder::default().root_id("app")
|
/// dioxus_server::ServeConfig::default().root_id("app")
|
||||||
/// })
|
/// })
|
||||||
/// // You also need to set the root id in your web config
|
/// // You also need to set the root id in your web config
|
||||||
/// .with_cfg(web! {
|
/// .with_cfg(web! {
|
||||||
@@ -134,12 +142,11 @@ impl ServeConfigBuilder {
|
|||||||
///
|
///
|
||||||
/// #[cfg(feature = "server")]
|
/// #[cfg(feature = "server")]
|
||||||
/// dioxus_server::serve(|| async move {
|
/// dioxus_server::serve(|| async move {
|
||||||
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
|
/// use dioxus_server::{axum, ServeConfig, DioxusRouterExt};
|
||||||
///
|
///
|
||||||
/// let config = ServeConfigBuilder::default()
|
/// let config = ServeConfig::default()
|
||||||
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
||||||
/// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn Any> + Send + Sync>]))
|
/// .context_providers(Arc::new(vec![Box::new(|| Box::new(1234u32) as Box<dyn Any>) as Box<dyn Fn() -> Box<dyn Any> + Send + Sync>]));
|
||||||
/// .build()?;
|
|
||||||
///
|
///
|
||||||
/// Ok(
|
/// Ok(
|
||||||
/// axum::Router::new()
|
/// axum::Router::new()
|
||||||
@@ -192,12 +199,11 @@ impl ServeConfigBuilder {
|
|||||||
///
|
///
|
||||||
/// #[cfg(feature = "server")]
|
/// #[cfg(feature = "server")]
|
||||||
/// dioxus_server::serve(|| async move {
|
/// dioxus_server::serve(|| async move {
|
||||||
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
|
/// use dioxus_server::{axum, ServeConfig, DioxusRouterExt};
|
||||||
///
|
///
|
||||||
/// let config = ServeConfigBuilder::default()
|
/// let config = ServeConfig::default()
|
||||||
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
||||||
/// .context_provider(|| 1234u32)
|
/// .context_provider(|| 1234u32);
|
||||||
/// .build()?;
|
|
||||||
///
|
///
|
||||||
/// Ok(
|
/// Ok(
|
||||||
/// axum::Router::new()
|
/// axum::Router::new()
|
||||||
@@ -243,12 +249,12 @@ impl ServeConfigBuilder {
|
|||||||
/// // Run a custom server with axum on the server
|
/// // Run a custom server with axum on the server
|
||||||
/// #[cfg(feature = "server")]
|
/// #[cfg(feature = "server")]
|
||||||
/// dioxus_server::serve(|| async move {
|
/// dioxus_server::serve(|| async move {
|
||||||
/// use dioxus_server::{axum, ServeConfigBuilder, DioxusRouterExt};
|
/// use dioxus_server::{axum, ServeConfig, DioxusRouterExt};
|
||||||
|
///
|
||||||
|
/// let config = ServeConfig::default()
|
||||||
|
/// // You can provide context to your whole app on the server (including server functions) with the `context_provider` method on the launch builder
|
||||||
|
/// .context(1234u32);
|
||||||
///
|
///
|
||||||
/// let config = ServeConfigBuilder::default()
|
|
||||||
/// .context(1234u32)
|
|
||||||
/// .build()?;
|
|
||||||
//
|
|
||||||
/// Ok(
|
/// Ok(
|
||||||
/// axum::Router::new()
|
/// axum::Router::new()
|
||||||
/// .serve_dioxus_application(config, app)
|
/// .serve_dioxus_application(config, app)
|
||||||
@@ -307,188 +313,4 @@ impl ServeConfigBuilder {
|
|||||||
self.streaming_mode = StreamingMode::OutOfOrder;
|
self.streaming_mode = StreamingMode::OutOfOrder;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the ServeConfig. This may fail if the index.html file is not found.
|
|
||||||
///
|
|
||||||
/// ## WASM compatibility
|
|
||||||
/// In the context of WASM the file system normally can't be read so this always requires
|
|
||||||
/// the `index_html` field to be set.
|
|
||||||
pub fn build(self) -> Result<ServeConfig, UnableToLoadIndex> {
|
|
||||||
let root_id = self.root_id.unwrap_or("main");
|
|
||||||
|
|
||||||
let index_html = match self.index_html {
|
|
||||||
Some(index) => index,
|
|
||||||
None => {
|
|
||||||
// The CLI always bundles static assets into the exe/public directory
|
|
||||||
let public_path = public_path();
|
|
||||||
|
|
||||||
let index_path = self
|
|
||||||
.index_path
|
|
||||||
.unwrap_or_else(|| public_path.join("index.html"));
|
|
||||||
load_index_path(index_path).unwrap_or_else(|_| {
|
|
||||||
r#"
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head> </head>
|
|
||||||
<body>
|
|
||||||
<div id="main"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"#
|
|
||||||
.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let index = load_index_html(index_html, root_id);
|
|
||||||
let context_providers = Arc::new(
|
|
||||||
self.context_providers
|
|
||||||
.into_iter()
|
|
||||||
.map(|f| {
|
|
||||||
Box::new(move || f()) as Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync>
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(ServeConfig {
|
|
||||||
index,
|
|
||||||
incremental: self.incremental,
|
|
||||||
context_providers,
|
|
||||||
streaming_mode: self.streaming_mode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryInto<ServeConfig> for ServeConfigBuilder {
|
|
||||||
type Error = UnableToLoadIndex;
|
|
||||||
|
|
||||||
fn try_into(self) -> Result<ServeConfig, Self::Error> {
|
|
||||||
self.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the path to the public assets directory to serve static files from
|
|
||||||
pub(crate) fn public_path() -> PathBuf {
|
|
||||||
if let Ok(path) = std::env::var("DIOXUS_PUBLIC_PATH") {
|
|
||||||
return PathBuf::from(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The CLI always bundles static assets into the exe/public directory
|
|
||||||
std::env::current_exe()
|
|
||||||
.expect("Failed to get current executable path")
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.join("public")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An error that can occur when loading the index.html file
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UnableToLoadIndex(PathBuf);
|
|
||||||
|
|
||||||
impl std::fmt::Display for UnableToLoadIndex {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built. Tried to open file at path: {:?}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for UnableToLoadIndex {}
|
|
||||||
|
|
||||||
fn load_index_path(path: PathBuf) -> Result<String, UnableToLoadIndex> {
|
|
||||||
let mut file = File::open(&path).map_err(|_| UnableToLoadIndex(path))?;
|
|
||||||
|
|
||||||
let mut contents = String::new();
|
|
||||||
file.read_to_string(&mut contents)
|
|
||||||
.expect("Failed to read index.html");
|
|
||||||
Ok(contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
|
||||||
let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."));
|
|
||||||
|
|
||||||
let post_main = post_main.split_once('>').unwrap_or_else(|| {
|
|
||||||
panic!("Failed to find closing > after id=\"{root_id}\" in index.html.")
|
|
||||||
});
|
|
||||||
|
|
||||||
let (pre_main, post_main) = (
|
|
||||||
pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">",
|
|
||||||
post_main.1.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let (head, close_head) = pre_main.split_once("</head>").unwrap_or_else(|| {
|
|
||||||
panic!("Failed to find closing </head> tag after id=\"{root_id}\" in index.html.")
|
|
||||||
});
|
|
||||||
let (head, close_head) = (head.to_string(), "</head>".to_string() + close_head);
|
|
||||||
|
|
||||||
let (post_main, after_closing_body_tag) =
|
|
||||||
post_main.split_once("</body>").unwrap_or_else(|| {
|
|
||||||
panic!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Strip out the head if it exists
|
|
||||||
let mut head_before_title = String::new();
|
|
||||||
let mut head_after_title = head;
|
|
||||||
let mut title = String::new();
|
|
||||||
if let Some((new_head_before_title, new_title)) = head_after_title.split_once("<title>") {
|
|
||||||
let (new_title, new_head_after_title) = new_title
|
|
||||||
.split_once("</title>")
|
|
||||||
.expect("Failed to find closing </title> tag after <title> in index.html.");
|
|
||||||
title = format!("<title>{new_title}</title>");
|
|
||||||
head_before_title = new_head_before_title.to_string();
|
|
||||||
head_after_title = new_head_after_title.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
IndexHtml {
|
|
||||||
head_before_title,
|
|
||||||
head_after_title,
|
|
||||||
title,
|
|
||||||
close_head,
|
|
||||||
post_main: post_main.to_string(),
|
|
||||||
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct IndexHtml {
|
|
||||||
pub(crate) head_before_title: String,
|
|
||||||
pub(crate) head_after_title: String,
|
|
||||||
pub(crate) title: String,
|
|
||||||
pub(crate) close_head: String,
|
|
||||||
pub(crate) post_main: String,
|
|
||||||
pub(crate) after_closing_body_tag: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The streaming mode to use while rendering the page
|
|
||||||
#[derive(Clone, Copy, Default, PartialEq)]
|
|
||||||
pub enum StreamingMode {
|
|
||||||
/// Streaming is disabled; all server futures should be resolved before hydrating the page on the client
|
|
||||||
#[default]
|
|
||||||
Disabled,
|
|
||||||
/// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client
|
|
||||||
/// as they resolve
|
|
||||||
OutOfOrder,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus_ssr`].
|
|
||||||
/// See [`ServeConfigBuilder`] to create a ServeConfig
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ServeConfig {
|
|
||||||
pub(crate) index: IndexHtml,
|
|
||||||
pub(crate) incremental: Option<IncrementalRendererConfig>,
|
|
||||||
pub(crate) context_providers: ContextProviders,
|
|
||||||
pub(crate) streaming_mode: StreamingMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LaunchConfig for ServeConfig {}
|
|
||||||
|
|
||||||
impl ServeConfig {
|
|
||||||
/// Create a new ServeConfig
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
pub fn new() -> Result<Self, UnableToLoadIndex> {
|
|
||||||
ServeConfigBuilder::new().build()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new builder for a ServeConfig
|
|
||||||
pub fn builder() -> ServeConfigBuilder {
|
|
||||||
ServeConfigBuilder::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
/// An `IndexHtml` represents the contents of an `index.html` file used to serve a web application.
|
||||||
|
///
|
||||||
|
/// This defines the static portion of your web application, typically generated by a tool like `dx`
|
||||||
|
/// in conjunction with wasm-bindgen.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct IndexHtml {
|
||||||
|
pub(crate) head_before_title: String,
|
||||||
|
pub(crate) head_after_title: String,
|
||||||
|
pub(crate) title: String,
|
||||||
|
pub(crate) close_head: String,
|
||||||
|
pub(crate) post_main: String,
|
||||||
|
pub(crate) after_closing_body_tag: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexHtml {
|
||||||
|
/// Create a new `IndexHtml` from the raw contents of an `index.html` file.
|
||||||
|
///
|
||||||
|
/// This function will parse the `index.html` and split it into sections for easier manipulation.
|
||||||
|
/// The `root_id` parameter specifies the id of the main application container (e.g., "main")
|
||||||
|
/// which your app will render into.
|
||||||
|
pub(crate) fn new(contents: &str, root_id: &'static str) -> Result<IndexHtml, anyhow::Error> {
|
||||||
|
let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\""))
|
||||||
|
.with_context(|| format!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."))?;
|
||||||
|
|
||||||
|
let post_main = post_main.split_once('>').with_context(|| {
|
||||||
|
format!("Failed to find closing > after id=\"{root_id}\" in index.html.")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (pre_main, post_main) = (
|
||||||
|
pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">",
|
||||||
|
post_main.1.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (head, close_head) = pre_main.split_once("</head>").with_context(|| {
|
||||||
|
format!("Failed to find closing </head> tag after id=\"{root_id}\" in index.html.")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (head, close_head) = (head.to_string(), "</head>".to_string() + close_head);
|
||||||
|
|
||||||
|
let (post_main, after_closing_body_tag) =
|
||||||
|
post_main.split_once("</body>").with_context(|| {
|
||||||
|
format!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Strip out the head if it exists
|
||||||
|
let mut head_before_title = String::new();
|
||||||
|
let mut head_after_title = head;
|
||||||
|
let mut title = String::new();
|
||||||
|
if let Some((new_head_before_title, new_title)) = head_after_title.split_once("<title>") {
|
||||||
|
let (new_title, new_head_after_title) = new_title
|
||||||
|
.split_once("</title>")
|
||||||
|
.context("Failed to find closing </title> tag after <title> in index.html.")?;
|
||||||
|
title = format!("<title>{new_title}</title>");
|
||||||
|
head_before_title = new_head_before_title.to_string();
|
||||||
|
head_after_title = new_head_after_title.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(IndexHtml {
|
||||||
|
head_before_title,
|
||||||
|
head_after_title,
|
||||||
|
title,
|
||||||
|
close_head,
|
||||||
|
post_main: post_main.to_string(),
|
||||||
|
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IndexHtml {
|
||||||
|
fn default() -> Self {
|
||||||
|
const DEFAULT: &str = r#"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head> </head>
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
|
||||||
|
Self::new(DEFAULT, "main").expect("Failed to load default index.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
//! A launch function that creates an axum router for the LaunchBuilder
|
//! A launch function that creates an axum router for the LaunchBuilder
|
||||||
|
|
||||||
use crate::{server::DioxusRouterExt, RenderHandleState, ServeConfig, ServeConfigBuilder};
|
use crate::IndexHtml;
|
||||||
|
use crate::{server::DioxusRouterExt, RenderHandleState, ServeConfig};
|
||||||
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::{Request, State},
|
extract::{Request, State},
|
||||||
response::IntoResponse,
|
|
||||||
routing::IntoMakeService,
|
routing::IntoMakeService,
|
||||||
|
Router,
|
||||||
};
|
};
|
||||||
use dioxus_cli_config::base_path;
|
use dioxus_cli_config::base_path;
|
||||||
use dioxus_core::Element;
|
use dioxus_core::Element;
|
||||||
@@ -19,11 +21,11 @@ use hyper_util::{
|
|||||||
rt::{TokioExecutor, TokioIo},
|
rt::{TokioExecutor, TokioIo},
|
||||||
service::TowerToHyperService,
|
service::TowerToHyperService,
|
||||||
};
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{any::Any, collections::HashMap, net::SocketAddr, prelude::rust_2024::Future};
|
use std::{any::Any, collections::HashMap, net::SocketAddr, prelude::rust_2024::Future};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio_util::task::LocalPoolHandle;
|
use tokio_util::task::LocalPoolHandle;
|
||||||
use tower::Service;
|
use tower::{Service, ServiceExt as _};
|
||||||
use tower::ServiceExt as _;
|
|
||||||
|
|
||||||
type ContextList = Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>;
|
type ContextList = Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>;
|
||||||
|
|
||||||
@@ -55,34 +57,32 @@ async fn serve_server(
|
|||||||
|
|
||||||
dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg));
|
dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg));
|
||||||
|
|
||||||
let platform_config = platform_config
|
let mut cfg = platform_config
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|cfg| {
|
.find_map(|cfg| cfg.downcast::<ServeConfig>().ok().map(|b| *b))
|
||||||
cfg.downcast::<ServeConfig>()
|
|
||||||
.map(|cfg| Result::Ok(*cfg))
|
|
||||||
.or_else(|cfg| {
|
|
||||||
cfg.downcast::<ServeConfigBuilder>()
|
|
||||||
.map(|builder| builder.build())
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.unwrap_or_else(ServeConfig::new);
|
.unwrap_or_else(ServeConfig::new);
|
||||||
|
|
||||||
// Extend the config's context providers with the context providers from the launch builder
|
// Extend the config's context providers with the context providers from the launch builder
|
||||||
let cfg = platform_config
|
for ctx in contexts {
|
||||||
.map(|mut cfg| {
|
let arced = Arc::new(ctx) as Arc<dyn Fn() -> Box<dyn Any> + Send + Sync>;
|
||||||
let mut contexts = contexts;
|
cfg.context_providers.push(arced);
|
||||||
let cfg_context_providers = cfg.context_providers.clone();
|
}
|
||||||
for i in 0..cfg_context_providers.len() {
|
|
||||||
contexts.push(Box::new({
|
// Create the IndexHtml object if the user has specified an override
|
||||||
let cfg_context_providers = cfg_context_providers.clone();
|
if let Some(raw_index_html) = cfg.index_html_override.as_ref() {
|
||||||
move || (cfg_context_providers[i])()
|
cfg.index = IndexHtml::new(raw_index_html, cfg.root_id.unwrap_or("main"))
|
||||||
}));
|
.expect("Failed to parse custom index.html");
|
||||||
|
} else if !cfg!(target_arch = "wasm32") {
|
||||||
|
let public_path = crate::public_path();
|
||||||
|
let index_html_path = public_path.join("index.html");
|
||||||
|
|
||||||
|
if index_html_path.exists() {
|
||||||
|
let index_html = std::fs::read_to_string(index_html_path)
|
||||||
|
.expect("Failed to read index.html from public directory");
|
||||||
|
cfg.index = IndexHtml::new(&index_html, cfg.root_id.unwrap_or("main"))
|
||||||
|
.expect("Failed to parse index.html from public directory");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cfg.context_providers = std::sync::Arc::new(contexts);
|
|
||||||
cfg
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
|
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
|
||||||
// and we use the generated address the CLI gives us
|
// and we use the generated address the CLI gives us
|
||||||
@@ -90,7 +90,7 @@ async fn serve_server(
|
|||||||
|
|
||||||
// Create the router and register the server functions under the basepath.
|
// Create the router and register the server functions under the basepath.
|
||||||
let router = apply_base_path(
|
let router = apply_base_path(
|
||||||
axum::Router::new().serve_dioxus_application(cfg.clone(), original_root),
|
Router::new().serve_dioxus_application(cfg.clone(), original_root),
|
||||||
original_root,
|
original_root,
|
||||||
cfg.clone(),
|
cfg.clone(),
|
||||||
base_path().map(|s| s.to_string()),
|
base_path().map(|s| s.to_string()),
|
||||||
@@ -101,6 +101,7 @@ async fn serve_server(
|
|||||||
.map(usize::from)
|
.map(usize::from)
|
||||||
.unwrap_or(1),
|
.unwrap_or(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut make_service = router.into_make_service();
|
let mut make_service = router.into_make_service();
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
||||||
@@ -136,7 +137,7 @@ async fn serve_server(
|
|||||||
let tcp_stream = TokioIo::new(tcp_stream);
|
let tcp_stream = TokioIo::new(tcp_stream);
|
||||||
|
|
||||||
std::future::poll_fn(|cx| {
|
std::future::poll_fn(|cx| {
|
||||||
<IntoMakeService<axum::Router> as tower::Service<Request>>::poll_ready(
|
<IntoMakeService<Router> as tower::Service<Request>>::poll_ready(
|
||||||
&mut make_service,
|
&mut make_service,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -189,8 +190,8 @@ async fn serve_server(
|
|||||||
|
|
||||||
unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() };
|
unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() };
|
||||||
|
|
||||||
let mut new_router = axum::Router::new().serve_static_assets();
|
let mut new_router = Router::new().serve_static_assets();
|
||||||
let new_cfg = ServeConfig::new().unwrap();
|
let new_cfg = ServeConfig::new();
|
||||||
|
|
||||||
let server_fn_iter = ServerFunction::collect();
|
let server_fn_iter = ServerFunction::collect();
|
||||||
|
|
||||||
@@ -250,29 +251,25 @@ async fn serve_server(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_base_path(
|
fn apply_base_path(
|
||||||
mut router: axum::Router,
|
mut router: Router,
|
||||||
root: fn() -> Result<VNode, RenderError>,
|
root: fn() -> Element,
|
||||||
cfg: ServeConfig,
|
cfg: ServeConfig,
|
||||||
base_path: Option<String>,
|
base_path: Option<String>,
|
||||||
) -> axum::Router {
|
) -> Router {
|
||||||
if let Some(base_path) = base_path {
|
if let Some(base_path) = base_path {
|
||||||
let base_path = base_path.trim_matches('/');
|
let base_path = base_path.trim_matches('/');
|
||||||
|
|
||||||
// If there is a base path, nest the router under it and serve the root route manually
|
// If there is a base path, nest the router under it and serve the root route manually
|
||||||
// Nesting a route in axum only serves /base_path or /base_path/ not both
|
// Nesting a route in axum only serves /base_path or /base_path/ not both
|
||||||
router = axum::Router::new().nest(&format!("/{base_path}/"), router);
|
router = Router::new().nest(&format!("/{base_path}/"), router).route(
|
||||||
|
&format!("/{base_path}"),
|
||||||
async fn root_render_handler(
|
axum::routing::method_routing::get(
|
||||||
state: State<RenderHandleState>,
|
|state: State<RenderHandleState>, mut request: Request<Body>| async move {
|
||||||
mut request: Request<Body>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
// The root of the base path always looks like the root from dioxus fullstack
|
// The root of the base path always looks like the root from dioxus fullstack
|
||||||
*request.uri_mut() = "/".parse().unwrap();
|
*request.uri_mut() = "/".parse().unwrap();
|
||||||
RenderHandleState::render_handler(state, request).await
|
RenderHandleState::render_handler(state, request).await
|
||||||
}
|
},
|
||||||
|
)
|
||||||
router = router.route(
|
|
||||||
&format!("/{base_path}"),
|
|
||||||
axum::routing::method_routing::get(root_render_handler)
|
|
||||||
.with_state(RenderHandleState::new(cfg, root)),
|
.with_state(RenderHandleState::new(cfg, root)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -280,28 +277,43 @@ fn apply_base_path(
|
|||||||
router
|
router
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve<F>(mut serve_it: impl FnMut() -> F)
|
/// Serve a fullstack dioxus application with a custom axum router.
|
||||||
|
///
|
||||||
|
/// This function sets up an async runtime, enables the default dioxus logger, runs the provided initializer,
|
||||||
|
/// and then starts an axum server with the returned router.
|
||||||
|
///
|
||||||
|
/// The axum router will be bound to the address specified by the `IP` and `PORT` environment variables,
|
||||||
|
/// defaulting to `127.0.0.1:8080` if not set.
|
||||||
|
pub fn serve<F>(mut serve_it: impl FnMut() -> F) -> !
|
||||||
where
|
where
|
||||||
F: Future<Output = Result<axum::Router, anyhow::Error>>,
|
F: Future<Output = Result<Router, anyhow::Error>>,
|
||||||
{
|
{
|
||||||
dioxus_logger::initialize_default();
|
dioxus_logger::initialize_default();
|
||||||
|
|
||||||
tokio::runtime::Runtime::new()
|
let app_future = async move {
|
||||||
.unwrap()
|
let router = serve_it().await.expect("Failed to create axum router");
|
||||||
.block_on(async move {
|
|
||||||
let router = serve_it().await.unwrap();
|
|
||||||
|
|
||||||
let address = dioxus_cli_config::fullstack_address_or_localhost();
|
let address = dioxus_cli_config::fullstack_address_or_localhost();
|
||||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(address)
|
||||||
|
|
||||||
tracing::trace!("Listening on {address}");
|
|
||||||
|
|
||||||
axum::serve::serve(listener, router.into_make_service())
|
|
||||||
.await
|
.await
|
||||||
|
.with_context(|| format!("Failed to bind app to given address: {address}"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
tracing::trace!("Listening on {address}");
|
||||||
|
axum::serve::serve(listener, router)
|
||||||
|
.await
|
||||||
|
.expect("Failed to serve axum app");
|
||||||
|
};
|
||||||
|
|
||||||
// unreachable!("Serving a fullstack app should never return")
|
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||||
|
handle.block_on(app_future);
|
||||||
|
} else {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(app_future);
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!("Serving a fullstack app should never return")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a router that serves the dioxus application at the appropriate base path.
|
/// Create a router that serves the dioxus application at the appropriate base path.
|
||||||
@@ -315,10 +327,10 @@ where
|
|||||||
/// - Hot-reloading
|
/// - Hot-reloading
|
||||||
/// - Async Runtime
|
/// - Async Runtime
|
||||||
/// - Logging
|
/// - Logging
|
||||||
pub fn router(app: fn() -> Element) -> axum::Router {
|
pub fn router(app: fn() -> Element) -> Router {
|
||||||
let cfg = ServeConfig::new().unwrap();
|
let cfg = ServeConfig::new();
|
||||||
apply_base_path(
|
apply_base_path(
|
||||||
axum::Router::new().serve_dioxus_application(cfg.clone(), app),
|
Router::new().serve_dioxus_application(cfg.clone(), app),
|
||||||
app,
|
app,
|
||||||
cfg,
|
cfg,
|
||||||
base_path().map(|s| s.to_string()),
|
base_path().map(|s| s.to_string()),
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ pub use bytes::Bytes;
|
|||||||
pub use dioxus_fullstack_core::{ServerFnError, ServerFnResult};
|
pub use dioxus_fullstack_core::{ServerFnError, ServerFnResult};
|
||||||
|
|
||||||
pub use axum;
|
pub use axum;
|
||||||
pub(crate) use config::*;
|
pub use config::ServeConfig;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use config::{ServeConfig, ServeConfigBuilder};
|
|
||||||
pub use document::ServerDocument;
|
pub use document::ServerDocument;
|
||||||
pub use http;
|
pub use http;
|
||||||
pub use inventory;
|
pub use inventory;
|
||||||
@@ -46,3 +45,6 @@ pub use serverfn::*;
|
|||||||
|
|
||||||
pub mod isrg;
|
pub mod isrg;
|
||||||
pub use isrg::*;
|
pub use isrg::*;
|
||||||
|
|
||||||
|
mod index_html;
|
||||||
|
pub(crate) use index_html::IndexHtml;
|
||||||
@@ -2,20 +2,17 @@ use crate::{
|
|||||||
ssr::{SSRError, SsrRendererPool},
|
ssr::{SSRError, SsrRendererPool},
|
||||||
ServeConfig, ServerFunction,
|
ServeConfig, ServerFunction,
|
||||||
};
|
};
|
||||||
use crate::{IncrementalRendererError, RenderFreshness};
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body,
|
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{Request, Response, StatusCode},
|
http::{Request, StatusCode},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
|
response::Response,
|
||||||
routing::*,
|
routing::*,
|
||||||
};
|
};
|
||||||
use dioxus_core::{Element, VirtualDom};
|
use dioxus_core::{Element, VirtualDom};
|
||||||
use dioxus_fullstack_core::HttpError;
|
use http::header::*;
|
||||||
use futures::Stream;
|
use std::path::{Path, PathBuf};
|
||||||
use http::{header::*, request::Parts};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tower::util::MapResponse;
|
use tower::util::MapResponse;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
@@ -46,10 +43,9 @@ use tower_http::services::fs::ServeFileSystemResponseBody;
|
|||||||
/// // Note you can use `register_server_functions_with_context`
|
/// // Note you can use `register_server_functions_with_context`
|
||||||
/// // to inject the context into server functions running outside
|
/// // to inject the context into server functions running outside
|
||||||
/// // of an SSR render context.
|
/// // of an SSR render context.
|
||||||
/// .fallback(get(render_handler)
|
/// .fallback(get(render_handler))
|
||||||
/// .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app))
|
/// .with_state(RenderHandleState::new(ServeConfig::new(), app));
|
||||||
/// )
|
///
|
||||||
/// .into_make_service();
|
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// axum::serve(listener, router).await.unwrap();
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// }
|
/// }
|
||||||
@@ -103,7 +99,7 @@ pub trait DioxusRouterExt<S>: DioxusRouterFnExt<S> {
|
|||||||
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
||||||
/// let router = axum::Router::new()
|
/// let router = axum::Router::new()
|
||||||
/// // Server side render the application, serve static assets, and register server functions
|
/// // Server side render the application, serve static assets, and register server functions
|
||||||
/// .serve_dioxus_application(ServeConfig::new().unwrap(), app)
|
/// .serve_dioxus_application(ServeConfig::new(), app)
|
||||||
/// .into_make_service();
|
/// .into_make_service();
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// axum::serve(listener, router).await.unwrap();
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
@@ -124,7 +120,7 @@ where
|
|||||||
S: Send + Sync + Clone + 'static,
|
S: Send + Sync + Clone + 'static,
|
||||||
{
|
{
|
||||||
fn serve_static_assets(self) -> Self {
|
fn serve_static_assets(self) -> Self {
|
||||||
let public_path = crate::public_path();
|
let public_path = public_path();
|
||||||
|
|
||||||
if !public_path.exists() {
|
if !public_path.exists() {
|
||||||
return self;
|
return self;
|
||||||
@@ -166,6 +162,7 @@ pub trait DioxusRouterFnExt<S> {
|
|||||||
fn register_server_functions(self) -> Self
|
fn register_server_functions(self) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
/// Serves a Dioxus application without static assets.
|
/// Serves a Dioxus application without static assets.
|
||||||
/// Sets up server function routes and rendering endpoints only.
|
/// Sets up server function routes and rendering endpoints only.
|
||||||
///
|
///
|
||||||
@@ -179,7 +176,7 @@ pub trait DioxusRouterFnExt<S> {
|
|||||||
/// #[tokio::main]
|
/// #[tokio::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let router = axum::Router::new()
|
/// let router = axum::Router::new()
|
||||||
/// .serve_api_application(ServeConfig::new().unwrap(), app)
|
/// .serve_api_application(ServeConfig::new(), app)
|
||||||
/// .into_make_service();
|
/// .into_make_service();
|
||||||
/// // ...
|
/// // ...
|
||||||
/// }
|
/// }
|
||||||
@@ -195,8 +192,14 @@ pub trait DioxusRouterFnExt<S> {
|
|||||||
|
|
||||||
impl<S: Send + Sync + Clone + 'static> DioxusRouterFnExt<S> for Router<S> {
|
impl<S: Send + Sync + Clone + 'static> DioxusRouterFnExt<S> for Router<S> {
|
||||||
fn register_server_functions(mut self) -> Self {
|
fn register_server_functions(mut self) -> Self {
|
||||||
for f in ServerFunction::collect() {
|
for func in ServerFunction::collect() {
|
||||||
self = f.register_server_fn_on_router(self);
|
tracing::info!(
|
||||||
|
"Registering server function: {} {}",
|
||||||
|
func.method(),
|
||||||
|
func.path()
|
||||||
|
);
|
||||||
|
|
||||||
|
self = func.register_server_fn_on_router(self);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -229,7 +232,8 @@ impl RenderHandleState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application.
|
/// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be
|
||||||
|
/// used to pass context into the root component of your application.
|
||||||
pub fn new_with_virtual_dom_factory(
|
pub fn new_with_virtual_dom_factory(
|
||||||
config: ServeConfig,
|
config: ServeConfig,
|
||||||
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||||
@@ -272,20 +276,21 @@ impl RenderHandleState {
|
|||||||
/// // Note you can use `register_server_functions_with_context`
|
/// // Note you can use `register_server_functions_with_context`
|
||||||
/// // to inject the context into server functions running outside
|
/// // to inject the context into server functions running outside
|
||||||
/// // of an SSR render context.
|
/// // of an SSR render context.
|
||||||
/// .fallback(get(render_handler)
|
/// .fallback(get(render_handler))
|
||||||
/// .with_state(RenderHandleState::new(ServeConfig::new().unwrap(), app))
|
/// .with_state(RenderHandleState::new(ServeConfig::new(), app))
|
||||||
/// )
|
|
||||||
/// .into_make_service();
|
/// .into_make_service();
|
||||||
|
///
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// axum::serve(listener, router).await.unwrap();
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn render_handler(
|
pub async fn render_handler(State(state): State<Self>, request: Request<Body>) -> Response {
|
||||||
State(state): State<Self>,
|
let (parts, _) = request.into_parts();
|
||||||
request: Request<Body>,
|
|
||||||
) -> impl IntoResponse {
|
let response = state
|
||||||
let cfg = &state.config;
|
.renderers
|
||||||
let build_virtual_dom = {
|
.clone()
|
||||||
|
.render_to(parts, &state.config, {
|
||||||
let build_virtual_dom = state.build_virtual_dom.clone();
|
let build_virtual_dom = state.build_virtual_dom.clone();
|
||||||
let context_providers = state.config.context_providers.clone();
|
let context_providers = state.config.context_providers.clone();
|
||||||
move || {
|
move || {
|
||||||
@@ -295,13 +300,12 @@ impl RenderHandleState {
|
|||||||
}
|
}
|
||||||
vdom
|
vdom
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
let (parts, _) = request.into_parts();
|
match response {
|
||||||
|
|
||||||
match state.render(parts, cfg, build_virtual_dom).await {
|
|
||||||
Ok((status, freshness, rx)) => {
|
Ok((status, freshness, rx)) => {
|
||||||
let mut response = axum::response::Response::builder()
|
let mut response = Response::builder()
|
||||||
.status(status.status)
|
.status(status.status)
|
||||||
.header(CONTENT_TYPE, "text/html; charset=utf-8")
|
.header(CONTENT_TYPE, "text/html; charset=utf-8")
|
||||||
.body(Body::from_stream(rx))
|
.body(Body::from_stream(rx))
|
||||||
@@ -309,15 +313,20 @@ impl RenderHandleState {
|
|||||||
|
|
||||||
freshness.write(response.headers_mut());
|
freshness.write(response.headers_mut());
|
||||||
|
|
||||||
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(SSRError::Incremental(e)) => {
|
Err(SSRError::Incremental(e)) => {
|
||||||
tracing::error!("Failed to render page: {}", e);
|
tracing::error!("Failed to render page: {}", e);
|
||||||
Ok(Self::err_to_axum_response(e).into_response())
|
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(e.to_string())
|
||||||
|
.unwrap()
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(SSRError::HttpError { status, message }) => Ok(Response::builder()
|
Err(SSRError::HttpError { status, message }) => Response::builder()
|
||||||
.status(status)
|
.status(status)
|
||||||
.body(Body::from(message.unwrap_or_else(|| {
|
.body(Body::from(message.unwrap_or_else(|| {
|
||||||
status
|
status
|
||||||
@@ -325,43 +334,26 @@ impl RenderHandleState {
|
|||||||
.unwrap_or("An unknown error occurred")
|
.unwrap_or("An unknown error occurred")
|
||||||
.to_string()
|
.to_string()
|
||||||
})))
|
})))
|
||||||
.unwrap()),
|
.unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn err_to_axum_response<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
|
|
||||||
Response::builder()
|
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
.body(body::Body::new(format!("Error: {}", e)))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the application to HTML.
|
|
||||||
pub async fn render<'a>(
|
|
||||||
&'a self,
|
|
||||||
parts: Parts,
|
|
||||||
cfg: &'a ServeConfig,
|
|
||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
|
||||||
) -> Result<
|
|
||||||
(
|
|
||||||
HttpError,
|
|
||||||
RenderFreshness,
|
|
||||||
impl Stream<Item = Result<String, IncrementalRendererError>>,
|
|
||||||
),
|
|
||||||
SSRError,
|
|
||||||
> {
|
|
||||||
self.renderers
|
|
||||||
.clone()
|
|
||||||
.render_to(parts, cfg, virtual_dom_factory)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serve_dir_cached<S>(
|
/// Get the path to the public assets directory to serve static files from
|
||||||
mut router: Router<S>,
|
pub(crate) fn public_path() -> PathBuf {
|
||||||
public_path: &std::path::Path,
|
if let Ok(path) = std::env::var("DIOXUS_PUBLIC_PATH") {
|
||||||
directory: &std::path::Path,
|
return PathBuf::from(path);
|
||||||
) -> Router<S>
|
}
|
||||||
|
|
||||||
|
// The CLI always bundles static assets into the exe/public directory
|
||||||
|
std::env::current_exe()
|
||||||
|
.expect("Failed to get current executable path")
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("public")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_dir_cached<S>(mut router: Router<S>, public_path: &Path, directory: &Path) -> Router<S>
|
||||||
where
|
where
|
||||||
S: Send + Sync + Clone + 'static,
|
S: Send + Sync + Clone + 'static,
|
||||||
{
|
{
|
||||||
@@ -376,8 +368,16 @@ where
|
|||||||
if path == public_path.join("index.html") {
|
if path == public_path.join("index.html") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let route = path.strip_prefix(public_path).unwrap();
|
|
||||||
let route = path_components_to_route_lossy(route);
|
let route = format!(
|
||||||
|
"/{}",
|
||||||
|
path.strip_prefix(public_path)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|segment| segment.to_string_lossy())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/")
|
||||||
|
);
|
||||||
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
router = serve_dir_cached(router, public_path, &path);
|
router = serve_dir_cached(router, public_path, &path);
|
||||||
@@ -397,15 +397,6 @@ where
|
|||||||
router
|
router
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_components_to_route_lossy(path: &Path) -> String {
|
|
||||||
let route = path
|
|
||||||
.iter()
|
|
||||||
.map(|segment| segment.to_string_lossy())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("/");
|
|
||||||
format!("/{}", route)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MappedAxumService<S> = MapResponse<
|
type MappedAxumService<S> = MapResponse<
|
||||||
S,
|
S,
|
||||||
fn(Response<ServeFileSystemResponseBody>) -> Response<ServeFileSystemResponseBody>,
|
fn(Response<ServeFileSystemResponseBody>) -> Response<ServeFileSystemResponseBody>,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::handler::Handler;
|
|
||||||
use axum::routing::MethodRouter;
|
use axum::routing::MethodRouter;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
@@ -19,10 +18,6 @@ pub struct ServerFunction<Caller = ()> {
|
|||||||
_phantom: PhantomData<Caller>,
|
_phantom: PhantomData<Caller>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MakeRequest<T> {
|
|
||||||
_phantom: PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerFunction {
|
impl ServerFunction {
|
||||||
/// Create a new server function object.
|
/// Create a new server function object.
|
||||||
pub const fn new(
|
pub const fn new(
|
||||||
@@ -52,56 +47,14 @@ impl ServerFunction {
|
|||||||
inventory::iter::<ServerFunction>().collect()
|
inventory::iter::<ServerFunction>().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handler(&self) -> fn() -> MethodRouter<DioxusServerState> {
|
||||||
|
self.handler
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register_server_fn_on_router<S>(&'static self, router: Router<S>) -> Router<S>
|
pub fn register_server_fn_on_router<S>(&'static self, router: Router<S>) -> Router<S>
|
||||||
where
|
where
|
||||||
S: Send + Sync + Clone + 'static,
|
S: Send + Sync + Clone + 'static,
|
||||||
{
|
{
|
||||||
tracing::info!(
|
|
||||||
"Registering server function: {} {}",
|
|
||||||
self.method(),
|
|
||||||
self.path()
|
|
||||||
);
|
|
||||||
use http::method::Method;
|
|
||||||
let path = self.path();
|
|
||||||
let method = self.method();
|
|
||||||
let handler = move |req| self.handle_server_fns_inner(req);
|
|
||||||
match method {
|
|
||||||
Method::GET => router.route(path, axum::routing::get(handler)),
|
|
||||||
Method::POST => router.route(path, axum::routing::post(handler)),
|
|
||||||
Method::PUT => router.route(path, axum::routing::put(handler)),
|
|
||||||
Method::DELETE => router.route(path, axum::routing::delete(handler)),
|
|
||||||
Method::PATCH => router.route(path, axum::routing::patch(handler)),
|
|
||||||
Method::HEAD => router.route(path, axum::routing::head(handler)),
|
|
||||||
Method::OPTIONS => router.route(path, axum::routing::options(handler)),
|
|
||||||
Method::CONNECT => router.route(path, axum::routing::connect(handler)),
|
|
||||||
Method::TRACE => router.route(path, axum::routing::trace(handler)),
|
|
||||||
_ => unimplemented!("Unsupported server function method: {}", method),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_server_fns_inner(&self, req: http::Request<Body>) -> http::Response<Body> {
|
|
||||||
// todo: jon
|
|
||||||
// - bring back middleware to serverfns. the new layer system is fine but isn't a full replacement
|
|
||||||
//
|
|
||||||
// use axum::body;
|
|
||||||
// use axum::extract::State;
|
|
||||||
// use axum::routing::*;
|
|
||||||
// use axum::{
|
|
||||||
// body::Body,
|
|
||||||
// http::{Request, Response, StatusCode},
|
|
||||||
// response::IntoResponse,
|
|
||||||
// };
|
|
||||||
// use http::header::*;
|
|
||||||
|
|
||||||
// let (parts, body) = req.into_parts();
|
|
||||||
// let req = Request::from_parts(parts.clone(), body);
|
|
||||||
|
|
||||||
// // Create the server context with info from the request
|
|
||||||
// let server_context = DioxusServerContext::new(parts);
|
|
||||||
|
|
||||||
// // Provide additional context from the render state
|
|
||||||
// server_context.add_server_context(&additional_context);
|
|
||||||
|
|
||||||
// // store Accepts and Referrer in case we need them for redirect (below)
|
// // store Accepts and Referrer in case we need them for redirect (below)
|
||||||
// let referrer = req.headers().get(REFERER).cloned();
|
// let referrer = req.headers().get(REFERER).cloned();
|
||||||
// let accepts_html = req
|
// let accepts_html = req
|
||||||
@@ -111,13 +64,6 @@ impl ServerFunction {
|
|||||||
// .map(|v| v.contains("text/html"))
|
// .map(|v| v.contains("text/html"))
|
||||||
// .unwrap_or(false);
|
// .unwrap_or(false);
|
||||||
|
|
||||||
// actually run the server fn (which may use the server context)
|
|
||||||
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
|
|
||||||
// let fut = crate::with_server_context(server_context.clone(), || service.run(req));
|
|
||||||
|
|
||||||
// let res = ProvideServerContext::new(fut, server_context.clone()).await;
|
|
||||||
// let mut res = res.res;
|
|
||||||
|
|
||||||
// // it it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
// // it it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
||||||
// // Location set, then redirect to Referer
|
// // Location set, then redirect to Referer
|
||||||
// if accepts_html {
|
// if accepts_html {
|
||||||
@@ -130,13 +76,10 @@ impl ServerFunction {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // apply the response parts from the server context to the response
|
router.route(
|
||||||
// server_context.send_response(&mut res);
|
self.path(),
|
||||||
|
((self.handler)()).with_state(DioxusServerState {}),
|
||||||
let mthd: MethodRouter<DioxusServerState> =
|
)
|
||||||
(self.handler)().with_state(DioxusServerState {});
|
|
||||||
|
|
||||||
mthd.call(req, DioxusServerState {}).await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -667,8 +667,6 @@ impl SsrRendererPool {
|
|||||||
to: &mut R,
|
to: &mut R,
|
||||||
virtual_dom: &VirtualDom,
|
virtual_dom: &VirtualDom,
|
||||||
) -> Result<(), IncrementalRendererError> {
|
) -> Result<(), IncrementalRendererError> {
|
||||||
let ServeConfig { index, .. } = cfg;
|
|
||||||
|
|
||||||
let title = {
|
let title = {
|
||||||
let document: Option<Rc<ServerDocument>> =
|
let document: Option<Rc<ServerDocument>> =
|
||||||
virtual_dom.in_scope(ScopeId::ROOT, dioxus_core::try_consume_context);
|
virtual_dom.in_scope(ScopeId::ROOT, dioxus_core::try_consume_context);
|
||||||
@@ -676,13 +674,13 @@ impl SsrRendererPool {
|
|||||||
document.and_then(|document| document.title())
|
document.and_then(|document| document.title())
|
||||||
};
|
};
|
||||||
|
|
||||||
to.write_str(&index.head_before_title)?;
|
to.write_str(&cfg.index.head_before_title)?;
|
||||||
if let Some(title) = title {
|
if let Some(title) = title {
|
||||||
to.write_str(&title)?;
|
to.write_str(&title)?;
|
||||||
} else {
|
} else {
|
||||||
to.write_str(&index.title)?;
|
to.write_str(&cfg.index.title)?;
|
||||||
}
|
}
|
||||||
to.write_str(&index.head_after_title)?;
|
to.write_str(&cfg.index.head_after_title)?;
|
||||||
|
|
||||||
let document =
|
let document =
|
||||||
virtual_dom.in_scope(ScopeId::ROOT, try_consume_context::<Rc<ServerDocument>>);
|
virtual_dom.in_scope(ScopeId::ROOT, try_consume_context::<Rc<ServerDocument>>);
|
||||||
@@ -704,9 +702,7 @@ impl SsrRendererPool {
|
|||||||
cfg: &ServeConfig,
|
cfg: &ServeConfig,
|
||||||
to: &mut R,
|
to: &mut R,
|
||||||
) -> Result<(), IncrementalRendererError> {
|
) -> Result<(), IncrementalRendererError> {
|
||||||
let ServeConfig { index, .. } = cfg;
|
to.write_str(&cfg.index.close_head)?;
|
||||||
|
|
||||||
to.write_str(&index.close_head)?;
|
|
||||||
|
|
||||||
// // #[cfg(feature = "document")]
|
// // #[cfg(feature = "document")]
|
||||||
// {
|
// {
|
||||||
@@ -723,8 +719,6 @@ impl SsrRendererPool {
|
|||||||
to: &mut R,
|
to: &mut R,
|
||||||
virtual_dom: &VirtualDom,
|
virtual_dom: &VirtualDom,
|
||||||
) -> Result<(), IncrementalRendererError> {
|
) -> Result<(), IncrementalRendererError> {
|
||||||
let ServeConfig { index, .. } = cfg;
|
|
||||||
|
|
||||||
// Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
|
// Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
|
||||||
// Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
|
// Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
|
||||||
let resolved_data = SsrRendererPool::serialize_server_data(virtual_dom, ScopeId::ROOT);
|
let resolved_data = SsrRendererPool::serialize_server_data(virtual_dom, ScopeId::ROOT);
|
||||||
@@ -749,7 +743,7 @@ impl SsrRendererPool {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
write!(to, r#"</script>"#,)?;
|
write!(to, r#"</script>"#,)?;
|
||||||
to.write_str(&index.post_main)?;
|
to.write_str(&cfg.index.post_main)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -759,9 +753,7 @@ impl SsrRendererPool {
|
|||||||
cfg: &ServeConfig,
|
cfg: &ServeConfig,
|
||||||
to: &mut R,
|
to: &mut R,
|
||||||
) -> Result<(), IncrementalRendererError> {
|
) -> Result<(), IncrementalRendererError> {
|
||||||
let ServeConfig { index, .. } = cfg;
|
to.write_str(&cfg.index.after_closing_body_tag)?;
|
||||||
|
|
||||||
to.write_str(&index.after_closing_body_tag)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,14 @@ impl<T: ?Sized, R: Deref<Target = T>> GenerationalRef<R> {
|
|||||||
guard: self.guard,
|
guard: self.guard,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clone the inner value. This requires that the inner value implements [`Clone`].
|
||||||
|
pub fn cloned(&self) -> T
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
self.inner.deref().clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ?Sized + Debug, R: Deref<Target = T>> Debug for GenerationalRef<R> {
|
impl<T: ?Sized + Debug, R: Deref<Target = T>> Debug for GenerationalRef<R> {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use dioxus_core::{use_hook, Callback, CapturedError, Result, Task};
|
|||||||
use dioxus_signals::{ReadSignal, ReadableBoxExt, ReadableExt, Signal, WritableExt};
|
use dioxus_signals::{ReadSignal, ReadableBoxExt, ReadableExt, Signal, WritableExt};
|
||||||
use futures_channel::oneshot::Receiver;
|
use futures_channel::oneshot::Receiver;
|
||||||
use futures_util::{future::Shared, FutureExt};
|
use futures_util::{future::Shared, FutureExt};
|
||||||
use std::{marker::PhantomData, prelude::rust_2024::Future};
|
use std::{marker::PhantomData, pin::Pin, prelude::rust_2024::Future, task::Poll};
|
||||||
|
|
||||||
pub fn use_action<E, C, M>(mut user_fn: C) -> Action<C::Input, C::Output>
|
pub fn use_action<E, C, M>(mut user_fn: C) -> Action<C::Input, C::Output>
|
||||||
where
|
where
|
||||||
@@ -82,15 +82,7 @@ pub struct Action<I, T: 'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<I: 'static, O: 'static> Action<I, O> {
|
impl<I: 'static, O: 'static> Action<I, O> {
|
||||||
pub fn value(&self) -> Option<ReadSignal<O>> {
|
pub fn value(&self) -> Option<Result<ReadSignal<O>, CapturedError>> {
|
||||||
if self.value.read().is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(self.reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn result(&self) -> Option<Result<ReadSignal<O>, CapturedError>> {
|
|
||||||
if !matches!(
|
if !matches!(
|
||||||
*self.state.read(),
|
*self.state.read(),
|
||||||
ActionState::Ready | ActionState::Errored
|
ActionState::Ready | ActionState::Errored
|
||||||
@@ -109,7 +101,7 @@ impl<I: 'static, O: 'static> Action<I, O> {
|
|||||||
Some(Ok(self.reader))
|
Some(Ok(self.reader))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_pending(&self) -> bool {
|
pub fn pending(&self) -> bool {
|
||||||
*self.state.read() == ActionState::Pending
|
*self.state.read() == ActionState::Pending
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,13 +154,10 @@ impl<T> Dispatching<T> {
|
|||||||
impl<T> std::future::Future for Dispatching<T> {
|
impl<T> std::future::Future for Dispatching<T> {
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn poll(
|
fn poll(mut self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||||
mut self: std::pin::Pin<&mut Self>,
|
|
||||||
_cx: &mut std::task::Context<'_>,
|
|
||||||
) -> std::task::Poll<Self::Output> {
|
|
||||||
match self.receiver.poll_unpin(_cx) {
|
match self.receiver.poll_unpin(_cx) {
|
||||||
std::task::Poll::Ready(_) => std::task::Poll::Ready(()),
|
Poll::Ready(_) => Poll::Ready(()),
|
||||||
std::task::Poll::Pending => std::task::Poll::Pending,
|
Poll::Pending => Poll::Pending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,6 +270,7 @@ impl<A: 'static, B: 'static, O> Action<(A, B), O> {
|
|||||||
Dispatching::new((self.callback).call((_a, _b)))
|
Dispatching::new((self.callback).call((_a, _b)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: 'static, B: 'static, C: 'static, O> Action<(A, B, C), O> {
|
impl<A: 'static, B: 'static, C: 'static, O> Action<(A, B, C), O> {
|
||||||
pub fn call(&mut self, _a: A, _b: B, _c: C) -> Dispatching<()> {
|
pub fn call(&mut self, _a: A, _b: B, _c: C) -> Dispatching<()> {
|
||||||
Dispatching::new((self.callback).call((_a, _b, _c)))
|
Dispatching::new((self.callback).call((_a, _b, _c)))
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use futures_util::{
|
|||||||
future::{self},
|
future::{self},
|
||||||
pin_mut, FutureExt, StreamExt,
|
pin_mut, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use std::ops::Deref;
|
|
||||||
use std::{cell::Cell, future::Future, rc::Rc};
|
use std::{cell::Cell, future::Future, rc::Rc};
|
||||||
|
use std::{fmt::Debug, ops::Deref};
|
||||||
|
|
||||||
#[doc = include_str!("../docs/use_resource.md")]
|
#[doc = include_str!("../docs/use_resource.md")]
|
||||||
#[doc = include_str!("../docs/rules_of_hooks.md")]
|
#[doc = include_str!("../docs/rules_of_hooks.md")]
|
||||||
@@ -324,6 +324,11 @@ impl<T> Resource<T> {
|
|||||||
self.task.cloned()
|
self.task.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is the resource's future currently running?
|
||||||
|
pub fn pending(&self) -> bool {
|
||||||
|
matches!(*self.state.peek(), UseResourceState::Pending)
|
||||||
|
}
|
||||||
|
|
||||||
/// Is the resource's future currently finished running?
|
/// Is the resource's future currently finished running?
|
||||||
///
|
///
|
||||||
/// Reading this does not subscribe to the future's state
|
/// Reading this does not subscribe to the future's state
|
||||||
@@ -439,6 +444,35 @@ impl<T> Resource<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T, E> Resource<Result<T, E>> {
|
||||||
|
/// Convert the `Resource<Result<T, E>>` into an `Option<Result<MappedSignal<T>, MappedSignal<E>>>`
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn result(
|
||||||
|
&self,
|
||||||
|
) -> Option<
|
||||||
|
Result<
|
||||||
|
MappedSignal<T, Signal<Option<Result<T, E>>>>,
|
||||||
|
MappedSignal<E, Signal<Option<Result<T, E>>>>,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
let value: MappedSignal<T, Signal<Option<Result<T, E>>>> = self.value.map(|v| match v {
|
||||||
|
Some(Ok(ref res)) => res,
|
||||||
|
_ => panic!("Resource is not ready"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let error: MappedSignal<E, Signal<Option<Result<T, E>>>> = self.value.map(|v| match v {
|
||||||
|
Some(Err(ref err)) => err,
|
||||||
|
_ => panic!("Resource is not ready"),
|
||||||
|
});
|
||||||
|
|
||||||
|
match &*self.value.peek() {
|
||||||
|
Some(Ok(_)) => Some(Ok(value)),
|
||||||
|
Some(Err(_)) => Some(Err(error)),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> From<Resource<T>> for ReadSignal<Option<T>> {
|
impl<T> From<Resource<T>> for ReadSignal<Option<T>> {
|
||||||
fn from(val: Resource<T>) -> Self {
|
fn from(val: Resource<T>) -> Self {
|
||||||
val.value.into()
|
val.value.into()
|
||||||
@@ -468,6 +502,19 @@ impl<T> Readable for Resource<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Writable for Resource<T> {
|
||||||
|
type WriteMetadata = <Signal<Option<T>> as Writable>::WriteMetadata;
|
||||||
|
|
||||||
|
fn try_write_unchecked(
|
||||||
|
&self,
|
||||||
|
) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError>
|
||||||
|
where
|
||||||
|
Self::Target: 'static,
|
||||||
|
{
|
||||||
|
self.value.try_write_unchecked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> IntoAttributeValue for Resource<T>
|
impl<T> IntoAttributeValue for Resource<T>
|
||||||
where
|
where
|
||||||
T: Clone + IntoAttributeValue,
|
T: Clone + IntoAttributeValue,
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ impl_event! [
|
|||||||
onmounted
|
onmounted
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pub use onmounted as onmount;
|
||||||
|
|
||||||
/// The MountedResult type for the MountedData
|
/// The MountedResult type for the MountedData
|
||||||
pub type MountedResult<T> = Result<T, MountedError>;
|
pub type MountedResult<T> = Result<T, MountedError>;
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ pub fn init(level: Level) -> Result<(), SetGlobalDefaultError> {
|
|||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
tracing_subscriber::EnvFilter::builder()
|
tracing_subscriber::EnvFilter::builder()
|
||||||
.with_default_directive(level.into())
|
.with_default_directive(level.into())
|
||||||
.from_env_lossy(),
|
.from_env_lossy()
|
||||||
|
.add_directive("hyper_util=warn".parse().unwrap()), // hyper has `debug!` sitting around in some places that are spammy
|
||||||
);
|
);
|
||||||
|
|
||||||
if !dioxus_cli_config::is_cli_enabled() {
|
if !dioxus_cli_config::is_cli_enabled() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::CopyValue;
|
use crate::{read::Readable, write_impls, ReadableRef, Signal};
|
||||||
use crate::{read::Readable, ReadableRef, Signal};
|
|
||||||
use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt};
|
use crate::{read_impls, GlobalMemo, ReadableExt, WritableExt};
|
||||||
|
use crate::{CopyValue, Writable};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
@@ -214,6 +214,19 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: 'static + PartialEq> Writable for Memo<T> {
|
||||||
|
type WriteMetadata = <Signal<T> as Writable>::WriteMetadata;
|
||||||
|
|
||||||
|
fn try_write_unchecked(
|
||||||
|
&self,
|
||||||
|
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError>
|
||||||
|
where
|
||||||
|
Self::Target: 'static,
|
||||||
|
{
|
||||||
|
self.inner.try_write_unchecked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> IntoAttributeValue for Memo<T>
|
impl<T> IntoAttributeValue for Memo<T>
|
||||||
where
|
where
|
||||||
T: Clone + IntoAttributeValue + PartialEq + 'static,
|
T: Clone + IntoAttributeValue + PartialEq + 'static,
|
||||||
@@ -250,6 +263,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
read_impls!(Memo<T> where T: PartialEq);
|
read_impls!(Memo<T> where T: PartialEq);
|
||||||
|
write_impls!(Memo<T> where T: PartialEq);
|
||||||
|
|
||||||
impl<T> Clone for Memo<T> {
|
impl<T> Clone for Memo<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
|
|||||||
Reference in new issue
Block a user