Merge pull request #916 from Demonthos/server-fn

Fullstack Utilities Crate (server functions)
This commit is contained in:
Jon Kelley 2023-05-18 13:18:49 +02:00 committed by GitHub
commit 3661682340
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 3854 additions and 27 deletions

View file

@ -22,6 +22,13 @@ members = [
"packages/rsx-rosetta",
"packages/signals",
"packages/hot-reload",
"packages/fullstack",
"packages/fullstack/server-macro",
"packages/fullstack/examples/axum-hello-world",
"packages/fullstack/examples/axum-router",
"packages/fullstack/examples/axum-desktop",
"packages/fullstack/examples/salvo-hello-world",
"packages/fullstack/examples/warp-hello-world",
"docs/guide",
# Full project examples
"examples/tailwind",

View file

@ -16,12 +16,12 @@ dioxus-native-core-macro = { path = "../../packages/native-core-macro" }
dioxus-router = { path = "../../packages/router" }
dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] }
dioxus-tui = { path = "../../packages/dioxus-tui" }
dioxus-fullstack = { path = "../../packages/fullstack" }
# dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] }
fermi = { path = "../../packages/fermi" }
shipyard = "0.6.2"
serde = { version = "1.0.138", features=["derive"] }
reqwest = { version = "0.11.11", features = ["json"] }
tokio = { version = "1.19.2" , features=[]}
tokio = { version = "1.19.2", features = ["full"] }
axum = { version = "0.6.1", features = ["ws"] }
gloo-storage = "0.2.2"

View file

@ -0,0 +1,34 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
#[cfg(feature = "ssr")]
{
use dioxus_fullstack::prelude::*;
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
.into_make_service(),
)
.await
.unwrap();
});
}
}
fn app(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
})
}

View file

@ -0,0 +1,74 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
// Get the root props from the document
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
use axum::extract::Path;
use axum::extract::State;
use axum::routing::get;
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
.serve_static_assets("./dist")
// Register server functions
.register_server_fns("")
// Connect to the hot reload server in debug mode
.connect_hot_reload()
// Render the application. This will serialize the root props (the intial count) into the HTML
.route(
"/",
get(move | State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
0,
)
.build(),
)
)}),
)
// Render the application with a different intial count
.route(
"/:initial_count",
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
intial_count,
)
.build(),
)
)}),
)
.with_state(SSRState::default())
.into_make_service(),
)
.await
.unwrap();
});
}
}
fn app(cx: Scope<usize>) -> Element {
let mut count = use_state(cx, || *cx.props);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
})
}

View file

@ -0,0 +1,30 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
#[tokio::main]
async fn main() {
#[cfg(feature = "ssr")]
{
use dioxus_fullstack::prelude::*;
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
.into_make_service(),
)
.await
.unwrap();
}
}
fn app(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
})
}

View file

@ -0,0 +1,112 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
// Get the root props from the document
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
use axum::extract::Path;
use axum::extract::State;
use axum::routing::get;
// Register the server function before starting the server
DoubleServer::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
.serve_static_assets("./dist")
// Register server functions
.register_server_fns("")
// Connect to the hot reload server in debug mode
.connect_hot_reload()
// Render the application. This will serialize the root props (the intial count) into the HTML
.route(
"/",
get(move |State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
0,
)
.build(),
)
)}),
)
// Render the application with a different intial count
.route(
"/:initial_count",
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
intial_count,
)
.build(),
)
)}),
)
.with_state(SSRState::default())
.into_make_service(),
)
.await
.unwrap();
});
}
}
fn app(cx: Scope<usize>) -> Element {
let mut count = use_state(cx, || *cx.props);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![count];
let sc = cx.sc();
async move {
// Call the server function just like a local async function
if let Ok(new_count) = double_server(sc, *count.current()).await {
count.set(new_count);
}
}
},
"Double"
}
})
}
// We use the "getcbor" encoding to make caching easier
#[server(DoubleServer, "", "getcbor")]
async fn double_server(cx: DioxusServerContext, number: usize) -> Result<usize, ServerFnError> {
// Perform some expensive computation or access a database on the server
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let result = number * 2;
println!(
"User Agent {:?}",
cx.request_parts().headers.get("User-Agent")
);
// Set the cache control header to 1 hour on the post request
cx.response_headers_mut()
.insert("Cache-Control", "max-age=3600".parse().unwrap());
println!("server calculated {result}");
Ok(result)
}

View file

@ -0,0 +1,137 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
#[cfg(feature = "ssr")]
#[derive(Default, Clone)]
struct ServerFunctionState {
call_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
// Get the root props from the document
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
use axum::body::Body;
use axum::extract::Path;
use axum::extract::State;
use axum::http::Request;
use axum::routing::get;
use std::sync::Arc;
// Register the server function before starting the server
DoubleServer::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
.serve_static_assets("./dist")
// Register server functions
.register_server_fns_with_handler("", |func| {
move |State(server_fn_state): State<ServerFunctionState>, req: Request<Body>| async move {
let (parts, body) = req.into_parts();
let parts: Arc<RequestParts> = Arc::new(parts.into());
let mut server_context = DioxusServerContext::new(parts.clone());
server_context.insert(server_fn_state);
server_fn_handler(server_context, func.clone(), parts, body).await
}
})
.with_state(ServerFunctionState::default())
// Connect to the hot reload server in debug mode
.connect_hot_reload()
// Render the application. This will serialize the root props (the intial count) into the HTML
.route(
"/",
get(move |State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
0,
)
.build(),
)
)}),
)
// Render the application with a different intial count
.route(
"/:initial_count",
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
intial_count,
)
.build(),
)
)}),
)
.with_state(SSRState::default())
.into_make_service(),
)
.await
.unwrap();
});
}
}
fn app(cx: Scope<usize>) -> Element {
let mut count = use_state(cx, || *cx.props);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![count];
let sc = cx.sc();
async move {
// Call the server function just like a local async function
if let Ok(new_count) = double_server(sc, *count.current()).await {
count.set(new_count);
}
}
},
"Double"
}
})
}
// We use the "getcbor" encoding to make caching easier
#[server(DoubleServer, "", "getcbor")]
async fn double_server(cx: DioxusServerContext, number: usize) -> Result<usize, ServerFnError> {
// Perform some expensive computation or access a database on the server
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let result = number * 2;
println!(
"User Agent {:?}",
cx.request_parts().headers.get("User-Agent")
);
// Set the cache control header to 1 hour on the post request
cx.response_headers_mut()
.insert("Cache-Control", "max-age=3600".parse().unwrap());
// Get the server function state
let server_fn_state = cx.get::<ServerFunctionState>().unwrap();
let call_count = server_fn_state
.call_count
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
println!("server functions have been called {call_count} times");
println!("server calculated {result}");
Ok(result)
}

View file

@ -0,0 +1,99 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
// Get the root props from the document
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
use axum::extract::Path;
use axum::extract::State;
use axum::routing::get;
// Register the server function before starting the server
DoubleServer::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
// Serve the dist folder with the static javascript and WASM files created by the dixous CLI
.serve_static_assets("./dist")
// Register server functions
.register_server_fns("")
// Connect to the hot reload server in debug mode
.connect_hot_reload()
// Render the application. This will serialize the root props (the intial count) into the HTML
.route(
"/",
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
intial_count,
)
.build(),
)
)}),
)
// Render the application with a different intial count
.route(
"/:initial_count",
get(move |Path(intial_count): Path<usize>, State(ssr_state): State<SSRState>| async move { axum::body::Full::from(
ssr_state.render(
&ServeConfigBuilder::new(
app,
intial_count,
)
.build(),
)
)}),
)
.with_state(SSRState::default())
.into_make_service(),
)
.await
.unwrap();
});
}
}
fn app(cx: Scope<usize>) -> Element {
let mut count = use_state(cx, || *cx.props);
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![count];
async move {
// Call the server function just like a local async function
if let Ok(new_count) = double_server(*count.current()).await {
count.set(new_count);
}
}
},
"Double"
}
})
}
#[server(DoubleServer)]
async fn double_server(number: usize) -> Result<usize, ServerFnError> {
// Perform some expensive computation or access a database on the server
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let result = number * 2;
println!("server calculated {result}");
Ok(result)
}

View file

@ -6,6 +6,7 @@
- [Desktop](getting_started/desktop.md)
- [Web](getting_started/web.md)
- [Server-Side Rendering](getting_started/ssr.md)
- [Fullstack](getting_started/fullstack.md)
- [Liveview](getting_started/liveview.md)
- [Terminal UI](getting_started/tui.md)
- [Mobile](getting_started/mobile.md)
@ -37,6 +38,12 @@
---
- [Fullstack](fullstack/index.md)
- [Getting Started](fullstack/getting_started.md)
- [Communicating with the Server](fullstack/server_functions.md)
---
- [Custom Renderer](custom_renderer/index.md)
---

View file

@ -0,0 +1,102 @@
> This guide assumes you read the [Web](web.md) guide and installed the [Dioxus-cli](https://github.com/DioxusLabs/cli)
# Getting Started
## Setup
For this guide, we're going to show how to use Dioxus with [Axum](https://docs.rs/axum/latest/axum/), but `dioxus-fullstack` also integrates with the [Warp](https://docs.rs/warp/latest/warp/) and [Salvo](https://docs.rs/salvo/latest/salvo/) web frameworks.
Make sure you have Rust and Cargo installed, and then create a new project:
```shell
cargo new --bin demo
cd demo
```
Add `dioxus` and `dioxus-fullstack` as dependencies:
```shell
cargo add dioxus
cargo add dioxus-fullstack --features axum, ssr
```
Next, add all the Axum dependencies. This will be different if you're using a different Web Framework
```shell
cargo add tokio --features full
cargo add axum
```
Your dependencies should look roughly like this:
```toml
[dependencies]
axum = "*"
dioxus = { version = "*" }
dioxus-fullstack = { version = "*", features = ["axum", "ssr"] }
tokio = { version = "*", features = ["full"] }
```
Now, set up your Axum app to serve the Dioxus app.
```rust
{{#include ../../../examples/server_basic.rs}}
```
Now, run your app with `cargo run` and open `http://localhost:8080` in your browser. You should see a server-side rendered page with a counter.
## Hydration
Right now, the page is static. We can't interact with the buttons. To fix this, we can hydrate the page with `dioxus-web`.
First, modify your `Cargo.toml` to include two features, one for the server called `ssr`, and one for the client called `web`.
```toml
[dependencies]
# Common dependancies
dioxus = { version = "*" }
dioxus-fullstack = { version = "*" }
# Web dependancies
dioxus-web = { version = "*", features=["hydrate"], optional = true }
# Server dependancies
axum = { version = "0.6.12", optional = true }
tokio = { version = "1.27.0", features = ["full"], optional = true }
[features]
default = []
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
web = ["dioxus-web"]
```
Next, we need to modify our `main.rs` to use either hydrate on the client or render on the server depending on the active features.
```rust
{{#include ../../../examples/hydration.rs}}
```
Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see the same page as before, but now you can interact with the buttons!
## Sycronizing props between the server and client
Let's make the initial count of the counter dynamic based on the current page.
### Modifying the server
To do this, we must remove the serve_dioxus_application and replace it with a custom implementation of its four key functions:
- Serve static WASM and JS files with serve_static_assets
- Register server functions with register_server_fns (more information on server functions later)
- Connect to the hot reload server with connect_hot_reload
- A custom route that uses SSRState to server-side render the application
### Modifying the client
The only thing we need to change on the client is the props. `dioxus-fullstack` will automatically serialize the props it uses to server render the app and send them to the client. In the client section of `main.rs`, we need to add `get_root_props_from_document` to deserialize the props before we hydrate the app.
```rust
{{#include ../../../examples/hydration_props.rs}}
```
Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. Navigate to `http://localhost:8080/1` and you should see the counter start at 1. Navigate to `http://localhost:8080/2` and you should see the counter start at 2.

View file

@ -0,0 +1,59 @@
# Fullstack development
So far you have learned about three different approaches to target the web with Dioxus:
- [Client-side rendering with dioxus-web](../getting_started/web.md)
- [Server-side rendering with dioxus-liveview](../getting_started/liveview.md)
- [Server-side static HTML generation with dioxus-ssr](../getting_started/ssr.md)
## Summary of Existing Approaches
Each approach has its tradeoffs:
### Client-side rendering
- With Client side rendering, you send the entire content of your application to the client, and then the client generates all of the HTML of the page dynamically.
- This means that the page will be blank until the JavaScript bundle has loaded and the application has initialized. This can result in **slower first render times and makes the page less SEO-friendly**.
> SEO stands for Search Engine Optimization. It refers to the practice of making your website more likely to appear in search engine results. Search engines like Google and Bing use web crawlers to index the content of websites. Most of these crawlers are not able to run JavaScript, so they will not be able to index the content of your page if it is rendered client-side.
- Client-side rendered applications need to use **weakly typed requests to communicate with the server**
> Client-side rendering is a good starting point for most applications. It is well supported and makes it easy to communicate with the client/browser APIs
### Liveview
- Liveview rendering communicates with the server over a WebSocket connection. It essentially moves all of the work that Client-side rendering does to the server.
- This makes it **easy to communicate with the server, but more difficult to communicate with the client/browser APIS**.
- Each interaction also requires a message to be sent to the server and back which can cause **issues with latency**.
- Because Liveview uses a websocket to render, the page will be blank until the WebSocket connection has been established and the first renderer has been sent form the websocket. Just like with client side rendering, this can make the page **less SEO-friendly**.
- Because the page is rendered on the server and the page is sent to the client piece by piece, you never need to send the entire application to the client. The initial load time can be faster than client-side rendering with large applications because Liveview only needs to send a constant small websocket script regardless of the size of the application.
> Liveview is a good fit for applications that already need to communicate with the server frequently (like real time collaborative apps), but don't need to communicate with as many client/browser APIs
### Server-side rendering
- Server-side rendering generates all of the HTML of the page on the server before the page is sent to the client. This means that the page will be fully rendered when it is sent to the client. This results in a faster first render time and makes the page more SEO-friendly. However, it **only works for static pages**.
> Server-side rendering is not a good fit for purely static sites like a blog
## A New Approach
Each of these approaches has its tradeoffs. What if we could combine the best parts of each approach?
- **Fast initial render** time like SSR
- **Works well with SEO** like SSR
- **Type safe easy communication with the server** like Liveview
- **Access to the client/browser APIs** like Client-side rendering
- **Fast interactivity** like Client-side rendering
We can achieve this by rendering the initial page on the server (SSR) and then taking over rendering on the client (Client-side rendering). Taking over rendering on the client is called **hydration**.
Finally, we can use [server functions](server_functions.md) to communicate with the server in a type-safe way.
This approach uses both the dioxus-web and dioxus-ssr crates. To integrate those two packages and `axum`, `warp`, or `salvo`, Dioxus provides the `dioxus-fullstack` crate.

View file

@ -0,0 +1,31 @@
# Communicating with the server
`dixous-server` provides server functions that allow you to call an automatically generated API on the server from the client as if it were a local function.
To make a server function, simply add the `#[server(YourUniqueType)]` attribute to a function. The function must:
- Be an async function
- Have arguments and a return type that both implement serialize and deserialize (with [serde](https://serde.rs/)).
- Return a `Result` with an error type of ServerFnError
You must call `register` on the type you passed into the server macro in your main function before starting your server to tell Dioxus about the server function.
Let's continue building on the app we made in the [getting started](./getting_started.md) guide. We will add a server function to our app that allows us to double the count on the server.
First, add serde as a dependency:
```shell
cargo add serde
```
Next, add the server function to your `main.rs`:
```rust
{{#include ../../../examples/server_function.rs}}
```
Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see a new button that multiplies the count by 2.
## Conclusion
That's it! You've created a full-stack Dioxus app. You can find more examples of full-stack apps and information about how to integrate with other frameworks and desktop renderers in the [dioxus-fullstack examples directory](https://github.com/DioxusLabs/dioxus/tree/master/packages/server/examples).

View file

@ -0,0 +1 @@
# Fullstack

View file

@ -5,28 +5,35 @@
3. Currently the cli only implements hot reloading for the web renderer. For TUI, desktop, and LiveView you can use the hot reload macro instead.
# Web
For the web renderer, you can use the dioxus cli to serve your application with hot reloading enabled.
## Setup
Install [dioxus-cli](https://github.com/DioxusLabs/cli).
Hot reloading is automatically enabled when using the web renderer on debug builds.
## Usage
1. Run:
```bash
```bash
dioxus serve --hot-reload
```
2. Change some code within a rsx or render macro
3. Open your localhost in a browser
4. Save and watch the style change without recompiling
# Desktop/Liveview/TUI
# Desktop/Liveview/TUI/Server
For desktop, LiveView, and tui, you can place the hot reload macro at the top of your main function to enable hot reloading.
Hot reloading is automatically enabled on debug builds.
For more information about hot reloading on native platforms and configuration options see the [dioxus-hot-reload](https://crates.io/crates/dioxus-hot-reload) crate.
## Setup
Add the following to your main function:
```rust
@ -37,13 +44,17 @@ fn main() {
```
## Usage
1. Run:
```bash
cargo run
```
2. Change some code within a rsx or render macro
3. Save and watch the style change without recompiling
# Limitations
1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will require a full recompile to capture the expression.
2. Components, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed.

View file

@ -1,17 +1,6 @@
# Server-Side Rendering
The Dioxus VirtualDom can be rendered server-side.
[Example: Dioxus DocSite](https://github.com/dioxusLabs/docsite)
## Multithreaded Support
The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc.
To solve this, you'll want to spawn a VirtualDom on its own thread and communicate with it via channels.
When working with web frameworks that require `Send`, it is possible to render a VirtualDom immediately to a String but you cannot hold the VirtualDom across an await point. For retained-state SSR (essentially LiveView), you'll need to create a pool of VirtualDoms.
For lower-level control over the rendering process, you can use the `dioxus-ssr` crate directly. This can be useful when integrating with a web framework that `dioxus-server` does not support, or pre-rendering pages.
## Setup
@ -21,7 +10,7 @@ Make sure you have Rust and Cargo installed, and then create a new project:
```shell
cargo new --bin demo
cd app
cd demo
```
Add Dioxus and the ssr renderer as dependencies:
@ -99,6 +88,8 @@ async fn app_endpoint() -> Html<String> {
}
```
And that's it!
## Multithreaded Support
> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it _must_ remain on the thread it started. We are working on loosening this requirement.
The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe.
When working with web frameworks that require `Send`, it is possible to render a VirtualDom immediately to a String but you cannot hold the VirtualDom across an await point. For retained-state SSR (essentially LiveView), you'll need to spawn a VirtualDom on its own thread and communicate with it via channels or create a pool of VirtualDoms.
You might notice that you cannot hold the VirtualDom across an await point. Because Dioxus is currently not ThreadSafe, it _must_ remain on the thread it started. We are working on loosening this requirement.

View file

@ -5,6 +5,7 @@ Build single-page applications that run in the browser with Dioxus. To run on th
A build of Dioxus for the web will be roughly equivalent to the size of a React build (70kb vs 65kb) but it will load significantly faster because [WebAssembly can be compiled as it is streamed](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/).
Examples:
- [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc)
- [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site)
@ -17,7 +18,7 @@ Examples:
The Web is the best-supported target platform for Dioxus.
- Because your app will be compiled to WASM you have access to browser APIs through [wasm-bingen](https://rustwasm.github.io/docs/wasm-bindgen/introduction.html).
- Dioxus provides hydration to resume apps that are rendered on the server. See the [hydration example](https://github.com/DioxusLabs/dioxus/blob/master/packages/web/examples/hydrate.rs) for more details.
- Dioxus provides hydration to resume apps that are rendered on the server. See the [fullstack](fullstack.md) getting started guide for more information.
## Tooling
@ -28,6 +29,7 @@ cargo install dioxus-cli
```
Make sure the `wasm32-unknown-unknown` target for rust is installed:
```shell
rustup target add wasm32-unknown-unknown
```
@ -49,11 +51,11 @@ cargo add dioxus-web
```
Edit your `main.rs`:
```rust
{{#include ../../../examples/hello_world_web.rs}}
```
And to serve our app:
```bash

1
packages/fullstack/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target

View file

@ -0,0 +1,64 @@
[package]
name = "dioxus-fullstack"
version = "0.1.0"
edition = "2021"
description = "Fullstack Dioxus Utilities"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# server functions
server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", features = ["stable"] }
dioxus_server_macro = { path = "server-macro" }
# warp
warp = { version = "0.3.3", optional = true }
http-body = { version = "0.4.5", optional = true }
# axum
axum = { version = "0.6.1", features = ["ws"], optional = true }
tower-http = { version = "0.4.0", optional = true, features = ["fs"] }
axum-macros = "0.3.7"
# salvo
salvo = { version = "0.37.7", optional = true, features = ["serve-static", "ws"] }
serde = "1.0.159"
# Dioxus + SSR
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true }
hyper = { version = "0.14.25", optional = true }
http = { version = "0.2.9", optional = true }
log = "0.4.17"
once_cell = "1.17.1"
thiserror = "1.0.40"
tokio = { version = "1.27.0", features = ["full"], optional = true }
object-pool = "0.5.4"
anymap = "0.12.1"
serde_json = { version = "1.0.95", optional = true }
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
futures-util = { version = "0.3.28", optional = true }
postcard = { version = "1.0.4", features = ["use-std"] }
yazi = "0.1.5"
base64 = "0.21.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { path = "../hot-reload" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
[features]
default = ["hot-reload"]
hot-reload = ["serde_json", "tokio-stream", "futures-util"]
warp = ["dep:warp", "http-body", "ssr"]
axum = ["dep:axum", "tower-http", "ssr"]
salvo = ["dep:salvo", "ssr"]
ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "hyper", "http"]

View file

@ -0,0 +1,108 @@
# Dioxus Fullstack
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-fullstack.svg
[crates-url]: https://crates.io/crates/dioxus-fullstack
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[API Docs](https://docs.rs/dioxus-fullstack/latest/dioxus_sever) |
[Chat](https://discord.gg/XgGxMSkvUM)
Fullstack utilities for the [`Dioxus`](https://dioxuslabs.com) framework.
# Features
- Intigrations with the [Axum](https::/docs.rs/dioxus-fullstack/latest/dixous_server/axum_adapter/index.html), [Salvo](https::/docs.rs/dioxus-fullstack/latest/dixous_server/salvo_adapter/index.html), and [Warp](https::/docs.rs/dioxus-fullstack/latest/dixous_server/warp_adapter/index.html) server frameworks with utilities for serving and rendering Dioxus applications.
- [Server functions](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/attr.server.html) allow you to call code on the server from the client as if it were a normal function.
- Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload).
- Passing root props from the server to the client.
# Example
Full stack Dioxus in under 50 lines of code
```rust
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
GetMeaning::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
warp::serve(
// Automatically handles server side rendering, hot reloading intigration, and hosting server functions
serve_dioxus_application(
"",
ServeConfigBuilder::new(app, ()),
)
)
.run(([127, 0, 0, 1], 8080))
.await;
});
}
}
fn app(cx: Scope) -> Element {
let meaning = use_state(cx, || None);
cx.render(rsx! {
button {
onclick: move |_| {
to_owned![meaning];
async move {
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
meaning.set(data);
}
}
},
"Run a server function"
}
"Server said: {meaning:?}"
})
}
// This code will only run on the server
#[server(GetMeaning)]
async fn get_meaning(of: String) -> Result<Option<u32>, ServerFnError> {
Ok(of.contains("life").then(|| 42))
}
```
## Getting Started
To get started with full stack Dioxus, check out our [getting started guide](https://dioxuslabs.com/docs/nightly/guide/en/getting_started/ssr.html), or the [full stack examples](https://github.com/DioxusLabs/dioxus/tree/master/packages/fullstack/examples).
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.

View file

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

View file

@ -0,0 +1,31 @@
[package]
name = "axum-desktop"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
[dependencies]
dioxus-desktop = { path = "../../../desktop", optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-router = { path = "../../../router" }
dioxus-fullstack = { path = "../../" }
axum = { version = "0.6.12", optional = true }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
[features]
default = []
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
desktop = ["dioxus-desktop"]
[[bin]]
name = "client"
path = "src/client.rs"
required-features = ["desktop"]
[[bin]]
name = "server"
path = "src/server.rs"
required-features = ["ssr"]

View file

@ -0,0 +1,13 @@
// Run with:
// ```bash
// cargo run --bin client --features="desktop"
// ```
use axum_desktop::*;
use dioxus_fullstack::prelude::server_fn::set_server_url;
fn main() {
// Set the url of the server where server functions are hosted.
set_server_url("http://localhost:8080");
dioxus_desktop::launch(app)
}

View file

@ -0,0 +1,40 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
pub fn app(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}

View file

@ -0,0 +1,23 @@
// Run with:
// ```bash
// cargo run --bin server --features="ssr"
// ```
use axum_desktop::*;
use dioxus_fullstack::prelude::*;
#[tokio::main]
async fn main() {
PostServerData::register().unwrap();
GetServerData::register().unwrap();
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.register_server_fns("")
.into_make_service(),
)
.await
.unwrap();
}

View file

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

View file

@ -0,0 +1,21 @@
[package]
name = "axum-hello-world"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-fullstack = { path = "../../" }
axum = { version = "0.6.12", optional = true }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
web = ["dioxus-web"]

View file

@ -0,0 +1,101 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }).build(),
)
.into_make_service(),
)
.await
.unwrap();
});
}
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
struct AppProps {
count: i32,
}
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text];
let sc = cx.sc();
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(sc, data).await.unwrap();
}
}
},
"Run a server function! testing1234"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}

View file

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

View file

@ -0,0 +1,24 @@
[package]
name = "axum-router"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-router = { path = "../../../router" }
dioxus-fullstack = { path = "../../" }
axum = { version = "0.6.12", optional = true }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
tower-http = { version = "0.4.0", features = ["fs"], optional = true }
http = { version = "0.2.9", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "http"]
web = ["dioxus-web", "dioxus-router/web"]

View file

@ -0,0 +1,153 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use dioxus_router::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
App,
AppProps { route: None },
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
use axum::extract::State;
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
// Serve the dist/assets folder with the javascript and WASM files created by the CLI
.serve_static_assets("./dist")
// Register server functions
.register_server_fns("")
// Connect to the hot reload server
.connect_hot_reload()
// If the path is unknown, render the application
.fallback(
move |uri: http::uri::Uri, State(ssr_state): State<SSRState>| {
let rendered = ssr_state.render(
&ServeConfigBuilder::new(
App,
AppProps {
route: Some(format!("http://{addr}{uri}")),
},
)
.build(),
);
async move { axum::body::Full::from(rendered) }
},
)
.with_state(SSRState::default())
.into_make_service(),
)
.await
.unwrap();
});
}
}
#[derive(Clone, Debug, Props, PartialEq, Serialize, Deserialize)]
struct AppProps {
route: Option<String>,
}
fn App(cx: Scope<AppProps>) -> Element {
cx.render(rsx! {
Router {
initial_url: cx.props.route.clone(),
Route { to: "/blog",
Link {
to: "/",
"Go to counter"
}
table {
tbody {
for _ in 0..100 {
tr {
for _ in 0..100 {
td { "hello world!" }
}
}
}
}
}
},
// Fallback
Route { to: "",
Counter {}
},
}
})
}
fn Counter(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
Link {
to: "/blog",
"Go to blog"
}
div{
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
}
})
}
#[server(PostServerData)]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}

View file

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

View file

@ -0,0 +1,21 @@
[package]
name = "salvo-hello-world"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-fullstack = { path = "../../" }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
salvo = { version = "0.37.9", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["salvo", "tokio", "dioxus-fullstack/salvo"]
web = ["dioxus-web"]

View file

@ -0,0 +1,97 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
use salvo::prelude::*;
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let router = Router::new().serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
);
Server::new(TcpListener::bind("127.0.0.1:8080"))
.serve(router)
.await;
});
}
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
struct AppProps {
count: i32,
}
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
let server_context = cx.sc();
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text, server_context];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(server_context, data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}

View file

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

View file

@ -0,0 +1,21 @@
[package]
name = "warp-hello-world"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-fullstack = { path = "../../" }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
warp = { version = "0.3.3", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["warp", "tokio", "dioxus-fullstack/warp"]
web = ["dioxus-web"]

View file

@ -0,0 +1,94 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let routes = serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
);
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
});
}
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
struct AppProps {
count: i32,
}
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
let server_context = cx.sc();
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 10, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text, server_context];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(server_context, data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}

View file

@ -0,0 +1,14 @@
[package]
name = "dioxus_server_macro"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
quote = "1.0.26"
server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", features = ["stable"] }
syn = { version = "1", features = ["full"] }
[lib]
proc-macro = true

View file

@ -0,0 +1,71 @@
use proc_macro::TokenStream;
use quote::ToTokens;
use server_fn_macro::*;
/// Declares that a function is a [server function](dioxus_fullstack). This means that
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
///
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
/// are enabled), it will instead make a network request to the server.
///
/// You can specify one, two, or three arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html),
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
/// server-side context into the server function.
///
/// ```ignore
/// # use dioxus_fullstack::prelude::*; use serde::{Serialize, Deserialize};
/// # #[derive(Serialize, Deserialize)]
/// # pub struct Post { }
/// #[server(ReadPosts, "/api")]
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database
/// todo!()
/// }
/// ```
///
/// Note the following:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
/// need to deserialize the result to return it to the client.
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
/// - **The [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function
/// can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html). This scope can be used to inject dependencies like the HTTP request
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let context = ServerContext {
ty: syn::parse_quote!(DioxusServerContext),
path: syn::parse_quote!(::dioxus_fullstack::prelude::DioxusServerContext),
};
match server_macro_impl(
args.into(),
s.into(),
Some(context),
Some(syn::parse_quote!(::dioxus_fullstack::prelude::server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}

View file

@ -0,0 +1,492 @@
//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! // Hydrate the application on the client
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "ssr")]
//! {
//! GetServerData::register().unwrap();
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
//! axum::Server::bind(&addr)
//! .serve(
//! axum::Router::new()
//! // Server side render the application, serve static assets, and register server functions
//! .serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
//! .into_make_service(),
//! )
//! .await
//! .unwrap();
//! });
//! }
//! }
//!
//! fn app(cx: Scope) -> Element {
//! let text = use_state(cx, || "...".to_string());
//!
//! cx.render(rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! })
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//! ```
use axum::{
body::{self, Body, BoxBody, Full},
extract::{State, WebSocketUpgrade},
handler::Handler,
http::{Request, Response, StatusCode},
response::IntoResponse,
routing::{get, post},
Router,
};
use dioxus_core::VirtualDom;
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext,
server_fn::DioxusServerFnRegistry,
};
/// A extension trait with utilities for integrating Dioxus with your Axum router.
pub trait DioxusRouterExt<S> {
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// .register_server_fns_with_handler("", |func| {
/// move |req: Request<Body>| async move {
/// let (parts, body) = req.into_parts();
/// let parts: Arc<RequestParts> = Arc::new(parts.into());
/// let server_context = DioxusServerContext::new(parts.clone());
/// server_fn_handler(server_context, func.clone(), parts, body).await
/// }
/// })
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
handler: impl Fn(ServerFunction) -> H,
) -> Self
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static;
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_fns("")
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// hot_reload_init!();
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Connect to hot reloading in debug mode
/// .connect_hot_reload()
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn connect_hot_reload(self) -> Self;
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_static_assets(ServeConfigBuilder::new(app, ()))
/// // Server render the application
/// // ...
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
///
/// fn app(cx: Scope) -> Element {
/// todo!()
/// }
/// ```
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self;
/// Serves the Dioxus application. This will serve a complete server side rendered application.
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
///
/// fn app(cx: Scope) -> Element {
/// todo!()
/// }
/// ```
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self;
}
impl<S> DioxusRouterExt<S> for Router<S>
where
S: Send + Sync + Clone + 'static,
{
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(ServerFunction) -> H,
) -> Self
where
H: Handler<T, S, Body>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}");
match func.encoding {
Encoding::Url | Encoding::Cbor => {
router = router.route(&full_route, post(handler(func)));
}
Encoding::GetJSON | Encoding::GetCBOR => {
router = router.route(&full_route, get(handler(func)));
}
}
}
router
}
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| {
move |req: Request<Body>| async move {
let (parts, body) = req.into_parts();
let parts: Arc<RequestParts> = Arc::new(parts.into());
let server_context = DioxusServerContext::new(parts.clone());
server_fn_handler(server_context, func.clone(), parts, body).await
}
})
}
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
use tower_http::services::{ServeDir, ServeFile};
let assets_path = assets_path.into();
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
let route = format!("/{}", route);
if path.is_dir() {
self = self.nest_service(&route, ServeDir::new(path));
} else {
self = self.nest_service(&route, ServeFile::new(path));
}
}
self
}
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self {
let cfg = cfg.into();
// Add server functions and render index.html
self.serve_static_assets(&cfg.assets_path)
.route(
"/",
get(render_handler).with_state((cfg, SSRState::default())),
)
.connect_hot_reload()
.register_server_fns(server_fn_route)
}
fn connect_hot_reload(self) -> Self {
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
{
self.nest(
"/_dioxus",
Router::new()
.route(
"/disconnect",
get(|ws: WebSocketUpgrade| async {
ws.on_upgrade(|mut ws| async move {
use axum::extract::ws::Message;
let _ = ws.send(Message::Text("connected".into())).await;
loop {
if ws.recv().await.is_none() {
break;
}
}
})
}),
)
.route("/hot_reload", get(hot_reload_handler)),
)
}
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
{
self
}
}
}
async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
request: Request<Body>,
) -> impl IntoResponse {
let (parts, _) = request.into_parts();
let parts: Arc<RequestParts> = Arc::new(parts.into());
let server_context = DioxusServerContext::new(parts);
let mut vdom =
VirtualDom::new_with_props(cfg.app, cfg.props.clone()).with_root_context(server_context);
let _ = vdom.rebuild();
let rendered = ssr_state.render_vdom(&vdom, &cfg);
Full::from(rendered)
}
/// A default handler for server functions. It will deserialize the request, call the server function, and serialize the response.
pub async fn server_fn_handler(
server_context: DioxusServerContext,
function: ServerFunction,
parts: Arc<RequestParts>,
body: Body,
) -> impl IntoResponse {
let body = hyper::body::to_bytes(body).await;
let Ok(body) = body else {
return report_err(body.err().unwrap());
};
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
let query_string = parts.uri.query().unwrap_or_default().to_string();
spawn_blocking({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async {
let query = &query_string.into();
let data = match &function.encoding {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let resp = match (function.trait_obj)(server_context.clone(), &data).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = parts
.headers
.get("Accept")
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
*res.headers_mut().expect("empty response should be valid") =
server_context.take_response_headers();
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
let resp = match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(body::boxed(Full::from(data))),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
)
.body(body::boxed(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(body::boxed(data)),
};
resp.unwrap()
}
Err(e) => report_err(e),
};
resp_tx.send(resp).unwrap();
})
}
});
resp_rx.await.unwrap()
}
fn report_err<E: Error>(e: E) -> Response<BoxBody> {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(body::boxed(format!("Error: {}", e)))
.unwrap()
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
pub async fn hot_reload_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
use axum::extract::ws::Message;
use futures_util::StreamExt;
let state = crate::hot_reload::spawn_hot_reload().await;
ws.on_upgrade(move |mut socket| async move {
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx =
tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver.clone());
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if socket.send(Message::Text(template)).await.is_err() {
break;
};
}
}
})
}

View file

@ -0,0 +1,18 @@
//! # Adapters
//! Adapters for different web frameworks.
//!
//! Each adapter provides a set of utilities that is ergonomic to use with the framework.
//!
//! Each framework has utilies for some or all of the following:
//! - Server functions
//! - A generic way to register server functions
//! - A way to register server functions with a custom handler that allows users to pass in a custom [`crate::server_context::DioxusServerContext`] based on the state of the server framework.
//! - A way to register static WASM files that is accepts [`crate::serve_config::ServeConfig`]
//! - A hot reloading web socket that intigrates with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload)
#[cfg(feature = "axum")]
pub mod axum_adapter;
#[cfg(feature = "salvo")]
pub mod salvo_adapter;
#[cfg(feature = "warp")]
pub mod warp_adapter;

View file

@ -0,0 +1,554 @@
//! Dioxus utilities for the [Salvo](https://salvo.rs) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "ssr")]
//! {
//! use salvo::prelude::*;
//! GetServerData::register().unwrap();
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let router =
//! Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
//! Server::new(TcpListener::bind("127.0.0.1:8080"))
//! .serve(router)
//! .await;
//! });
//! }
//! }
//!
//! fn app(cx: Scope) -> Element {
//! let text = use_state(cx, || "...".to_string());
//!
//! cx.render(rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! })
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//! ```
use dioxus_core::VirtualDom;
use hyper::{http::HeaderValue, StatusCode};
use salvo::{
async_trait, handler,
serve_static::{StaticDir, StaticFile},
Depot, FlowCtrl, Handler, Request, Response, Router,
};
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
};
/// A extension trait with utilities for integrating Dioxus with your Salvo router.
pub trait DioxusRouterExt {
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// struct ServerFunctionHandler {
/// server_fn: ServerFunction,
/// }
///
/// #[handler]
/// impl ServerFunctionHandler {
/// async fn handle(
/// &self,
/// req: &mut Request,
/// depot: &mut Depot,
/// res: &mut Response,
/// flow: &mut FlowCtrl,
/// ) {
/// // Add the headers to server context
/// ServerFnHandler::new((req.headers().clone(),), self.server_fn.clone())
/// .handle(req, depot, res, flow)
/// .await
/// }
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .register_server_fns_with_handler("", |func| {
/// ServerFnHandler::new(DioxusServerContext::default(), func)
/// });
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
/// ```
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
handler: impl Fn(ServerFunction) -> H,
) -> Self
where
H: Handler + 'static;
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .register_server_fns("");
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
///
/// ```
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .connect_hot_reload();
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
fn connect_hot_reload(self) -> Self;
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .server_static_assets("/dist");
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
/// ```
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self;
/// Serves the Dioxus application. This will serve a complete server side rendered application.
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
///
/// fn app(cx: Scope) -> Element {todo!()}
/// ```
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_path: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self;
}
impl DioxusRouterExt for Router {
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(ServerFunction) -> H,
) -> Self
where
H: Handler + 'static,
{
let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}");
match func.encoding {
Encoding::Url | Encoding::Cbor => {
router = router.push(Router::with_path(&full_route).post(handler(func)));
}
Encoding::GetJSON | Encoding::GetCBOR => {
router = router.push(Router::with_path(&full_route).get(handler(func)));
}
}
}
router
}
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| ServerFnHandler {
server_context: DioxusServerContext::default(),
function: func,
})
}
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
let assets_path = assets_path.into();
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
if path.is_file() {
let route = format!("/{}", route);
let serve_dir = StaticFile::new(path.clone());
self = self.push(Router::with_path(route).get(serve_dir))
} else {
let route = format!("/{}/<**path>", route);
let serve_dir = StaticDir::new([path.clone()]);
self = self.push(Router::with_path(route).get(serve_dir))
}
}
self
}
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_path: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self {
let cfg = cfg.into();
self.serve_static_assets(&cfg.assets_path)
.connect_hot_reload()
.register_server_fns(server_fn_path)
.push(Router::with_path("/").get(SSRHandler { cfg }))
}
fn connect_hot_reload(self) -> Self {
let mut _dioxus_router = Router::with_path("_dioxus");
_dioxus_router = _dioxus_router
.push(Router::with_path("hot_reload").handle(HotReloadHandler::default()));
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
{
_dioxus_router = _dioxus_router.push(Router::with_path("disconnect").handle(ignore_ws));
}
self.push(_dioxus_router)
}
}
/// Extracts the parts of a request that are needed for server functions. This will take parts of the request and replace them with empty values.
pub fn extract_parts(req: &mut Request) -> RequestParts {
RequestParts {
method: std::mem::take(req.method_mut()),
uri: std::mem::take(req.uri_mut()),
version: req.version(),
headers: std::mem::take(req.headers_mut()),
extensions: std::mem::take(req.extensions_mut()),
}
}
struct SSRHandler<P: Clone> {
cfg: ServeConfig<P>,
}
#[async_trait]
impl<P: Clone + serde::Serialize + Send + Sync + 'static> Handler for SSRHandler<P> {
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
_flow: &mut FlowCtrl,
) {
// Get the SSR renderer from the depot or create a new one if it doesn't exist
let renderer_pool = if let Some(renderer) = depot.obtain::<SSRState>() {
renderer.clone()
} else {
let renderer = SSRState::default();
depot.inject(renderer.clone());
renderer
};
let parts: Arc<RequestParts> = Arc::new(extract_parts(req));
let server_context = DioxusServerContext::new(parts);
let mut vdom = VirtualDom::new_with_props(self.cfg.app, self.cfg.props.clone())
.with_root_context(server_context.clone());
let _ = vdom.rebuild();
res.write_body(renderer_pool.render_vdom(&vdom, &self.cfg))
.unwrap();
*res.headers_mut() = server_context.take_response_headers();
}
}
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub struct ServerFnHandler {
server_context: DioxusServerContext,
function: ServerFunction,
}
impl ServerFnHandler {
/// Create a new server function handler with the given server context and server function.
pub fn new(server_context: impl Into<DioxusServerContext>, function: ServerFunction) -> Self {
let server_context = server_context.into();
Self {
server_context,
function,
}
}
}
#[handler]
impl ServerFnHandler {
async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let Self {
server_context,
function,
} = self;
let query = req
.uri()
.query()
.unwrap_or_default()
.as_bytes()
.to_vec()
.into();
let body = hyper::body::to_bytes(req.body_mut().unwrap()).await;
let Ok(body)=body else {
handle_error(body.err().unwrap(), res);
return;
};
let headers = req.headers();
let accept_header = headers.get("Accept").cloned();
let parts = Arc::new(extract_parts(req));
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
spawn_blocking({
let function = function.clone();
let mut server_context = server_context.clone();
server_context.parts = parts;
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async move {
let data = match &function.encoding {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let resp = (function.trait_obj)(server_context, data).await;
resp_tx.send(resp).unwrap();
})
}
});
let result = resp_rx.await.unwrap();
// Set the headers from the server context
*res.headers_mut() = server_context.take_response_headers();
match result {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok());
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res.set_status_code(StatusCode::OK);
}
match serialized {
Payload::Binary(data) => {
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static("application/cbor"));
res.write_body(data).unwrap();
}
Payload::Url(data) => {
res.headers_mut().insert(
"Content-Type",
HeaderValue::from_static(
"application/\
x-www-form-urlencoded",
),
);
res.write_body(data).unwrap();
}
Payload::Json(data) => {
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static("application/json"));
res.write_body(data).unwrap();
}
}
}
Err(err) => handle_error(err, res),
}
}
}
fn handle_error(error: impl Error + Send + Sync, res: &mut Response) {
let mut resp_err = Response::new();
resp_err.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
resp_err.render(format!("Internal Server Error: {}", error));
*res = resp_err;
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
#[derive(Default)]
pub struct HotReloadHandler;
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
#[handler]
impl HotReloadHandler {
async fn handle(
&self,
_req: &mut Request,
_depot: &mut Depot,
_res: &mut Response,
) -> Result<(), salvo::http::StatusError> {
Err(salvo::http::StatusError::not_found())
}
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
#[derive(Default)]
pub struct HotReloadHandler;
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
#[handler]
impl HotReloadHandler {
async fn handle(
&self,
req: &mut Request,
_depot: &mut Depot,
res: &mut Response,
) -> Result<(), salvo::http::StatusError> {
use salvo::ws::Message;
use salvo::ws::WebSocketUpgrade;
let state = crate::hot_reload::spawn_hot_reload().await;
WebSocketUpgrade::new()
.upgrade(req, res, move |mut websocket| async move {
use futures_util::StreamExt;
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if websocket
.send(Message::text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
state.message_receiver.clone(),
);
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if websocket.send(Message::text(template)).await.is_err() {
break;
};
}
}
})
.await
}
}
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
#[handler]
async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::http::StatusError> {
use salvo::ws::WebSocketUpgrade;
WebSocketUpgrade::new()
.upgrade(req, res, |mut ws| async move {
let _ = ws.send(salvo::ws::Message::text("connected")).await;
while let Some(msg) = ws.recv().await {
if msg.is_err() {
return;
};
}
})
.await
}

View file

@ -0,0 +1,444 @@
//! Dioxus utilities for the [Warp](https://docs.rs/warp/latest/warp/index.html) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "ssr")]
//! {
//! GetServerData::register().unwrap();
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
//! warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
//! });
//! }
//! }
//!
//! fn app(cx: Scope) -> Element {
//! let text = use_state(cx, || "...".to_string());
//!
//! cx.render(rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! })
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//!
//! ```
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
};
use dioxus_core::VirtualDom;
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use warp::path::FullPath;
use warp::{
filters::BoxedFilter,
http::{Response, StatusCode},
hyper::body::Bytes,
path, Filter, Reply,
};
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use warp::{body, header, hyper::HeaderMap, path, post, Filter};
///
/// #[tokio::main]
/// async fn main() {
/// let routes = register_server_fns_with_handler("", |full_route, func| {
/// path(full_route)
/// .and(post())
/// .and(header::headers_cloned())
/// .and(body::bytes())
/// .and_then(move |headers: HeaderMap, body| {
/// let func = func.clone();
/// async move {
/// // Add the headers to the server function context
/// server_fn_handler((headers.clone(),), func, headers, body).await
/// }
/// })
/// });
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn register_server_fns_with_handler<H, F, R>(
server_fn_route: &'static str,
mut handler: H,
) -> BoxedFilter<(R,)>
where
H: FnMut(String, ServerFunction) -> F,
F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
F::Extract: Send,
R: Reply + 'static,
{
let mut filter: Option<BoxedFilter<F::Extract>> = None;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}")
.trim_start_matches('/')
.to_string();
let route = handler(full_route, func.clone()).boxed();
if let Some(boxed_filter) = filter.take() {
filter = Some(boxed_filter.or(route).unify().boxed());
} else {
filter = Some(route);
}
}
filter.expect("No server functions found")
}
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = register_server_fns("");
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
register_server_fns_with_handler(server_fn_route, |full_route, func| {
path(full_route.clone())
.and(warp::post().or(warp::get()).unify())
.and(request_parts())
.and(warp::body::bytes())
.and_then(move |parts, bytes| {
let func = func.clone();
async move {
server_fn_handler(DioxusServerContext::default(), func, parts, bytes).await
}
})
})
}
/// Serves the Dioxus application. This will serve a complete server side rendered application.
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
///
/// fn app(cx: Scope) -> Element {
/// todo!()
/// }
/// ```
pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> BoxedFilter<(impl Reply,)> {
let cfg = cfg.into();
// Serve the dist folder and the index.html file
let serve_dir = warp::fs::dir(cfg.assets_path);
connect_hot_reload()
.or(register_server_fns(server_fn_route))
.or(warp::path::end().and(render_ssr(cfg)))
.or(serve_dir)
.boxed()
}
/// Server render the application.
pub fn render_ssr<P: Clone + serde::Serialize + Send + Sync + 'static>(
cfg: ServeConfig<P>,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
warp::get()
.and(request_parts())
.and(with_ssr_state())
.map(move |parts, renderer: SSRState| {
let parts = Arc::new(parts);
let server_context = DioxusServerContext::new(parts);
let mut vdom = VirtualDom::new_with_props(cfg.app, cfg.props.clone())
.with_root_context(server_context.clone());
let _ = vdom.rebuild();
let html = renderer.render_vdom(&vdom, &cfg);
let mut res = Response::builder();
*res.headers_mut().expect("empty request should be valid") =
server_context.take_response_headers();
res.header("Content-Type", "text/html")
.body(Bytes::from(html))
.unwrap()
})
}
/// An extractor for the request parts (used in [DioxusServerContext]). This will extract the method, uri, query, and headers from the request.
pub fn request_parts(
) -> impl Filter<Extract = (RequestParts,), Error = warp::reject::Rejection> + Clone {
warp::method()
.and(warp::filters::path::full())
.and(
warp::filters::query::raw()
.or(warp::any().map(String::new))
.unify(),
)
.and(warp::header::headers_cloned())
.and_then(move |method, path: FullPath, query, headers| async move {
http::uri::Builder::new()
.path_and_query(format!("{}?{}", path.as_str(), query))
.build()
.map_err(|err| {
warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err)))
})
.map(|uri| RequestParts {
method,
uri,
headers,
..Default::default()
})
})
}
fn with_ssr_state() -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone
{
let state = SSRState::default();
warp::any().map(move || state.clone())
}
#[derive(Debug)]
struct FailedToReadBody(String);
impl warp::reject::Reject for FailedToReadBody {}
#[derive(Debug)]
struct RecieveFailed(String);
impl warp::reject::Reject for RecieveFailed {}
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub async fn server_fn_handler(
server_context: impl Into<DioxusServerContext>,
function: ServerFunction,
parts: RequestParts,
body: Bytes,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let mut server_context = server_context.into();
let parts = Arc::new(parts);
server_context.parts = parts.clone();
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
spawn_blocking({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async move {
let query = parts
.uri
.query()
.unwrap_or_default()
.as_bytes()
.to_vec()
.into();
let data = match &function.encoding {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let resp = match (function.trait_obj)(server_context.clone(), data).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = parts
.headers
.get("Accept")
.as_ref()
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
*res.headers_mut().expect("empty request should be valid") =
server_context.take_response_headers();
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
let resp = match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(Bytes::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
)
.body(Bytes::from(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(Bytes::from(data)),
};
Box::new(resp.unwrap())
}
Err(e) => report_err(e),
};
if resp_tx.send(resp).is_err() {
eprintln!("Error sending response");
}
})
}
});
resp_rx.await.map_err(|err| {
warp::reject::custom(RecieveFailed(format!("Failed to recieve response {err}")))
})
}
fn report_err<E: Error>(e: E) -> Box<dyn warp::Reply> {
Box::new(
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("Error: {}", e))
.unwrap(),
) as Box<dyn warp::Reply>
}
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = connect_hot_reload();
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn connect_hot_reload() -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone
{
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
{
warp::path!("_dioxus" / "hot_reload")
.and(warp::ws())
.map(warp::reply)
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
}
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
{
use crate::hot_reload::HotReloadState;
use futures_util::sink::SinkExt;
use futures_util::StreamExt;
use warp::ws::Message;
let hot_reload = warp::path!("_dioxus" / "hot_reload")
.and(warp::any().then(|| crate::hot_reload::spawn_hot_reload()))
.and(warp::ws())
.map(move |state: &'static HotReloadState, ws: warp::ws::Ws| {
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
ws.on_upgrade(move |mut websocket| {
async move {
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if websocket
.send(Message::text(
serde_json::to_string(&template).unwrap(),
))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
state.message_receiver.clone(),
);
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if websocket.send(Message::text(template)).await.is_err() {
break;
};
}
}
}
})
});
let disconnect =
warp::path!("_dioxus" / "disconnect")
.and(warp::ws())
.map(move |ws: warp::ws::Ws| {
println!("disconnect");
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
ws.on_upgrade(move |mut websocket| async move {
struct DisconnectOnDrop(Option<warp::ws::WebSocket>);
impl Drop for DisconnectOnDrop {
fn drop(&mut self) {
let _ = self.0.take().unwrap().close();
}
}
let _ = websocket.send(Message::text("connected")).await;
let mut ws = DisconnectOnDrop(Some(websocket));
loop {
if ws.0.as_mut().unwrap().next().await.is_none() {
break;
}
}
})
});
disconnect.or(hot_reload)
}
}

View file

@ -0,0 +1,61 @@
use std::sync::Arc;
use dioxus_core::Template;
use tokio::sync::{
watch::{channel, Receiver},
RwLock,
};
#[derive(Clone)]
pub struct HotReloadState {
// The cache of all templates that have been modified since the last time we checked
pub(crate) templates: Arc<RwLock<std::collections::HashSet<dioxus_core::Template<'static>>>>,
// The channel to send messages to the hot reload thread
pub(crate) message_receiver: Receiver<Option<Template<'static>>>,
}
impl Default for HotReloadState {
fn default() -> Self {
let templates = Arc::new(RwLock::new(std::collections::HashSet::new()));
let (tx, rx) = channel(None);
dioxus_hot_reload::connect({
let templates = templates.clone();
move |msg| match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
{
let mut templates = templates.blocking_write();
templates.insert(template);
}
if let Err(err) = tx.send(Some(template)) {
log::error!("Failed to send hot reload message: {}", err);
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
}
});
Self {
templates,
message_receiver: rx,
}
}
}
// Hot reloading can be expensive to start so we spawn a new thread
static HOT_RELOAD_STATE: tokio::sync::OnceCell<HotReloadState> = tokio::sync::OnceCell::const_new();
pub(crate) async fn spawn_hot_reload() -> &'static HotReloadState {
HOT_RELOAD_STATE
.get_or_init(|| async {
println!("spinning up hot reloading");
let r = tokio::task::spawn_blocking(HotReloadState::default)
.await
.unwrap();
println!("hot reloading ready");
r
})
.await
}

View file

@ -0,0 +1,42 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)]
pub use adapters::*;
mod props_html;
mod adapters;
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
mod hot_reload;
#[cfg(feature = "ssr")]
mod render;
#[cfg(feature = "ssr")]
mod serve_config;
mod server_context;
mod server_fn;
/// A prelude of commonly used items in dioxus-fullstack.
pub mod prelude {
#[cfg(feature = "axum")]
pub use crate::adapters::axum_adapter::*;
#[cfg(feature = "salvo")]
pub use crate::adapters::salvo_adapter::*;
#[cfg(feature = "warp")]
pub use crate::adapters::warp_adapter::*;
#[cfg(not(feature = "ssr"))]
pub use crate::props_html::deserialize_props::get_root_props_from_document;
#[cfg(feature = "ssr")]
pub use crate::render::SSRState;
#[cfg(feature = "ssr")]
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
#[cfg(feature = "ssr")]
pub use crate::server_context::RequestParts;
pub use crate::server_context::{DioxusServerContext, HasServerContext};
pub use crate::server_fn::ServerFn;
#[cfg(feature = "ssr")]
pub use crate::server_fn::{ServerFnTraitObj, ServerFunction};
pub use dioxus_server_macro::*;
pub use server_fn::{self, ServerFn as _, ServerFnError};
}

View file

@ -0,0 +1,32 @@
use serde::de::DeserializeOwned;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
#[allow(unused)]
pub(crate) fn serde_from_string<T: DeserializeOwned>(string: &str) -> Option<T> {
let decompressed = STANDARD.decode(string.as_bytes()).ok()?;
let (decompressed, _) = yazi::decompress(&decompressed, yazi::Format::Zlib).unwrap();
postcard::from_bytes(&decompressed).ok()
}
#[cfg(not(feature = "ssr"))]
/// Get the props from the document. This is only available in the browser.
///
/// When dioxus-fullstack renders the page, it will serialize the root props and put them in the document. This function gets them from the document.
pub fn get_root_props_from_document<T: DeserializeOwned>() -> Option<T> {
#[cfg(not(target_arch = "wasm32"))]
{
None
}
#[cfg(target_arch = "wasm32")]
{
let attribute = web_sys::window()?
.document()?
.get_element_by_id("dioxus-storage")?
.get_attribute("data-serialized")?;
serde_from_string(&attribute)
}
}

View file

@ -0,0 +1,54 @@
pub(crate) mod deserialize_props;
pub(crate) mod serialize_props;
#[test]
fn serialized_and_deserializes() {
use postcard::to_allocvec;
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
struct Data {
a: u32,
b: String,
bytes: Vec<u8>,
nested: Nested,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
struct Nested {
a: u32,
b: u16,
c: u8,
}
for x in 0..10usize {
for y in 0..10 {
let mut as_string = String::new();
let data = vec![
Data {
a: x as u32,
b: "hello".to_string(),
bytes: vec![0; x],
nested: Nested {
a: 1,
b: x as u16,
c: 3
},
};
y
];
serialize_props::serde_to_writable(&data, &mut as_string).unwrap();
println!("{}", as_string);
println!(
"original size: {}",
std::mem::size_of::<Data>() * data.len()
);
println!("serialized size: {}", to_allocvec(&data).unwrap().len());
println!("compressed size: {}", as_string.len());
let decoded: Vec<Data> = deserialize_props::serde_from_string(&as_string).unwrap();
assert_eq!(data, decoded);
}
}
}

View file

@ -0,0 +1,31 @@
use serde::Serialize;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
#[allow(unused)]
pub(crate) fn serde_to_writable<T: Serialize>(
value: &T,
mut write_to: impl std::fmt::Write,
) -> std::fmt::Result {
let serialized = postcard::to_allocvec(value).unwrap();
let compressed = yazi::compress(
&serialized,
yazi::Format::Zlib,
yazi::CompressionLevel::BestSize,
)
.unwrap();
write_to.write_str(&STANDARD.encode(compressed));
Ok(())
}
#[cfg(feature = "ssr")]
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
pub(crate) fn encode_in_element<T: Serialize>(
data: T,
mut write_to: impl std::fmt::Write,
) -> std::fmt::Result {
write_to.write_str(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#)?;
serde_to_writable(&data, &mut write_to)?;
write_to.write_str(r#"" />"#)
}

View file

@ -0,0 +1,100 @@
//! A shared pool of renderers for efficient server side rendering.
use std::sync::Arc;
use dioxus_core::VirtualDom;
use dioxus_ssr::Renderer;
use crate::prelude::ServeConfig;
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
#[derive(Clone)]
pub struct SSRState {
// We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move
renderers: Arc<object_pool::Pool<Renderer>>,
}
impl Default for SSRState {
fn default() -> Self {
Self {
renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)),
}
}
}
impl SSRState {
/// Render the application to HTML.
pub fn render<P: 'static + Clone + serde::Serialize>(&self, cfg: &ServeConfig<P>) -> String {
let ServeConfig { app, props, .. } = cfg;
let mut vdom = VirtualDom::new_with_props(*app, props.clone());
let _ = vdom.rebuild();
self.render_vdom(&vdom, cfg)
}
/// Render a VirtualDom to HTML.
pub fn render_vdom<P: 'static + Clone + serde::Serialize>(
&self,
vdom: &VirtualDom,
cfg: &ServeConfig<P>,
) -> String {
let ServeConfig { index, .. } = cfg;
let mut renderer = self.renderers.pull(pre_renderer);
let mut html = String::new();
html += &index.pre_main;
let _ = renderer.render_to(&mut html, vdom);
// serialize the props
let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html);
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
// In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
let disconnect_js = r#"(function () {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = protocol + '//' + window.location.host + '/_dioxus/disconnect';
const poll_interval = 1000;
const reload_upon_connect = () => {
console.log('Disconnected from server. Attempting to reconnect...');
window.setTimeout(
() => {
// Try to reconnect to the websocket
const ws = new WebSocket(url);
ws.onopen = () => {
// If we reconnect, reload the page
window.location.reload();
}
// Otherwise, try again in a second
reload_upon_connect();
},
poll_interval);
};
// on initial page load connect to the disconnect ws
const ws = new WebSocket(url);
// if we disconnect, start polling
ws.onclose = reload_upon_connect;
})()"#;
html += r#"<script>"#;
html += disconnect_js;
html += r#"</script>"#;
}
html += &index.post_main;
html
}
}
fn pre_renderer() -> Renderer {
let mut renderer = Renderer::default();
renderer.pre_render = true;
renderer
}

View file

@ -0,0 +1,115 @@
//! Configeration for how to serve a Dioxus application
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use dioxus_core::Component;
/// 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)]
pub struct ServeConfigBuilder<P: Clone> {
pub(crate) app: Component<P>,
pub(crate) props: P,
pub(crate) root_id: Option<&'static str>,
pub(crate) index_path: Option<&'static str>,
pub(crate) assets_path: Option<&'static str>,
}
impl<P: Clone> ServeConfigBuilder<P> {
/// Create a new ServeConfigBuilder with the root component and props to render on the server.
pub fn new(app: Component<P>, props: P) -> Self {
Self {
app,
props,
root_id: None,
index_path: None,
assets_path: None,
}
}
/// 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: &'static str) -> Self {
self.index_path = Some(index_path);
self
}
/// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main)
pub fn root_id(mut self, root_id: &'static str) -> Self {
self.root_id = Some(root_id);
self
}
/// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist)
pub fn assets_path(mut self, assets_path: &'static str) -> Self {
self.assets_path = Some(assets_path);
self
}
/// Build the ServeConfig
pub fn build(self) -> ServeConfig<P> {
let assets_path = self.assets_path.unwrap_or("dist");
let index_path = self
.index_path
.map(PathBuf::from)
.unwrap_or_else(|| format!("{assets_path}/index.html").into());
let root_id = self.root_id.unwrap_or("main");
let index = load_index_html(index_path, root_id);
ServeConfig {
app: self.app,
props: self.props,
index,
assets_path,
}
}
}
fn load_index_html(path: PathBuf, root_id: &'static str) -> IndexHtml {
let mut file = File::open(path).expect("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built.");
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("Failed to read index.html");
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(),
);
IndexHtml {
pre_main,
post_main,
}
}
#[derive(Clone)]
pub(crate) struct IndexHtml {
pub(crate) pre_main: String,
pub(crate) post_main: String,
}
/// 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<P: Clone> {
pub(crate) app: Component<P>,
pub(crate) props: P,
pub(crate) index: IndexHtml,
pub(crate) assets_path: &'static str,
}
impl<P: Clone> From<ServeConfigBuilder<P>> for ServeConfig<P> {
fn from(builder: ServeConfigBuilder<P>) -> Self {
builder.build()
}
}

View file

@ -0,0 +1,160 @@
use dioxus_core::ScopeState;
/// A trait for an object that contains a server context
pub trait HasServerContext {
/// Get the server context from the state
fn server_context(&self) -> DioxusServerContext;
/// A shortcut for `self.server_context()`
fn sc(&self) -> DioxusServerContext {
self.server_context()
}
}
impl HasServerContext for &ScopeState {
fn server_context(&self) -> DioxusServerContext {
#[cfg(feature = "ssr")]
{
self.consume_context().expect("No server context found")
}
#[cfg(not(feature = "ssr"))]
{
DioxusServerContext {}
}
}
}
/// A shared context for server functions that contains infomation about the request and middleware state.
/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions.
///
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
#[derive(Clone)]
pub struct DioxusServerContext {
#[cfg(feature = "ssr")]
shared_context: std::sync::Arc<
std::sync::RwLock<anymap::Map<dyn anymap::any::Any + Send + Sync + 'static>>,
>,
#[cfg(feature = "ssr")]
headers: std::sync::Arc<std::sync::RwLock<hyper::header::HeaderMap>>,
#[cfg(feature = "ssr")]
pub(crate) parts: std::sync::Arc<RequestParts>,
}
#[allow(clippy::derivable_impls)]
impl Default for DioxusServerContext {
fn default() -> Self {
Self {
#[cfg(feature = "ssr")]
shared_context: std::sync::Arc::new(std::sync::RwLock::new(anymap::Map::new())),
#[cfg(feature = "ssr")]
headers: Default::default(),
#[cfg(feature = "ssr")]
parts: Default::default(),
}
}
}
#[cfg(feature = "ssr")]
pub use server_fn_impl::*;
#[cfg(feature = "ssr")]
mod server_fn_impl {
use super::*;
use std::sync::LockResult;
use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
use anymap::{any::Any, Map};
type SendSyncAnyMap = Map<dyn Any + Send + Sync + 'static>;
impl DioxusServerContext {
/// Create a new server context from a request
pub fn new(parts: impl Into<Arc<RequestParts>>) -> Self {
Self {
parts: parts.into(),
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
headers: Default::default(),
}
}
/// Clone a value from the shared server context
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
self.shared_context.read().ok()?.get::<T>().cloned()
}
/// Insert a value into the shared server context
pub fn insert<T: Any + Send + Sync + 'static>(
&mut self,
value: T,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
self.shared_context
.write()
.map(|mut map| map.insert(value))
.map(|_| ())
}
/// Get the headers from the server context
pub fn response_headers(&self) -> RwLockReadGuard<'_, hyper::header::HeaderMap> {
self.try_response_headers()
.expect("Failed to get headers from server context")
}
/// Try to get the headers from the server context
pub fn try_response_headers(
&self,
) -> LockResult<RwLockReadGuard<'_, hyper::header::HeaderMap>> {
self.headers.read()
}
/// Get the headers mutably from the server context
pub fn response_headers_mut(&self) -> RwLockWriteGuard<'_, hyper::header::HeaderMap> {
self.try_response_headers_mut()
.expect("Failed to get headers mutably from server context")
}
/// Try to get the headers mut from the server context
pub fn try_response_headers_mut(
&self,
) -> LockResult<RwLockWriteGuard<'_, hyper::header::HeaderMap>> {
self.headers.write()
}
pub(crate) fn take_response_headers(&self) -> hyper::header::HeaderMap {
let mut headers = self.headers.write().unwrap();
std::mem::take(&mut *headers)
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts(&self) -> &RequestParts {
&self.parts
}
}
/// Associated parts of an HTTP Request
#[derive(Debug, Default)]
pub struct RequestParts {
/// The request's method
pub method: http::Method,
/// The request's URI
pub uri: http::Uri,
/// The request's version
pub version: http::Version,
/// The request's headers
pub headers: http::HeaderMap<http::HeaderValue>,
/// The request's extensions
pub extensions: http::Extensions,
}
impl From<http::request::Parts> for RequestParts {
fn from(parts: http::request::Parts) -> Self {
Self {
method: parts.method,
uri: parts.uri,
version: parts.version,
headers: parts.headers,
extensions: parts.extensions,
}
}
}
}

View file

@ -0,0 +1,118 @@
use crate::server_context::DioxusServerContext;
#[cfg(any(feature = "ssr", doc))]
/// A trait object for a function that be called on serializable arguments and returns a serializable result.
pub type ServerFnTraitObj = server_fn::ServerFnTraitObj<DioxusServerContext>;
#[cfg(any(feature = "ssr", doc))]
/// A server function that can be called on serializable arguments and returns a serializable result.
pub type ServerFunction = server_fn::ServerFunction<DioxusServerContext>;
#[cfg(any(feature = "ssr", doc))]
#[allow(clippy::type_complexity)]
static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
std::sync::Arc<std::sync::RwLock<std::collections::HashMap<&'static str, ServerFunction>>>,
> = once_cell::sync::Lazy::new(Default::default);
#[cfg(any(feature = "ssr", doc))]
/// The registry of all Dioxus server functions.
pub struct DioxusServerFnRegistry;
#[cfg(any(feature = "ssr"))]
impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRegistry {
type Error = ServerRegistrationFnError;
fn register(
url: &'static str,
server_function: std::sync::Arc<ServerFnTraitObj>,
encoding: server_fn::Encoding,
) -> Result<(), Self::Error> {
// store it in the hashmap
let mut write = REGISTERED_SERVER_FUNCTIONS
.write()
.map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
let prev = write.insert(
url,
ServerFunction {
trait_obj: server_function,
encoding,
},
);
// if there was already a server function with this key,
// return Err
match prev {
Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!(
"There was already a server function registered at {:?}. \
This can happen if you use the same server function name \
in two different modules
on `stable` or in `release` mode.",
url
))),
None => Ok(()),
}
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get(url: &str) -> Option<ServerFunction> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).cloned())
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get_trait_obj(url: &str) -> Option<std::sync::Arc<ServerFnTraitObj>> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).map(|f| f.trait_obj.clone()))
}
fn get_encoding(url: &str) -> Option<server_fn::Encoding> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).map(|f| f.encoding.clone()))
}
/// Returns a list of all registered server functions.
fn paths_registered() -> Vec<&'static str> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.map(|fns| fns.keys().cloned().collect())
.unwrap_or_default()
}
}
#[cfg(any(feature = "ssr", doc))]
/// Errors that can occur when registering a server function.
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ServerRegistrationFnError {
/// The server function is already registered.
#[error("The server function {0} is already registered")]
AlreadyRegistered(String),
/// The server function registry is poisoned.
#[error("The server function registry is poisoned: {0}")]
Poisoned(String),
}
/// Defines a "server function." A server function can be called from the server or the client,
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
///
/// Server functions are created using the `server` macro.
///
/// The function should be registered by calling `ServerFn::register()`. The set of server functions
/// can be queried on the server for routing purposes by calling [server_fn::ServerFunctionRegistry::get].
///
/// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself.
pub trait ServerFn: server_fn::ServerFn<DioxusServerContext> {
/// Registers the server function, allowing the client to query it by URL.
#[cfg(any(feature = "ssr", doc))]
fn register() -> Result<(), server_fn::ServerFnError> {
Self::register_in::<DioxusServerFnRegistry>()
}
}
impl<T> ServerFn for T where T: server_fn::ServerFn<DioxusServerContext> {}

View file

@ -29,7 +29,9 @@ pub struct RouterProps<'a> {
pub active_class: Option<&'a str>,
/// Set the initial url.
pub initial_url: Option<String>,
// This is Option<Option<String>> because we want to be able to either omit the prop or pass in Option<String>
#[props(into)]
pub initial_url: Option<Option<String>>,
}
/// A component that conditionally renders children based on the current location of the app.
@ -45,7 +47,7 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
RouterCfg {
base_url: cx.props.base_url.map(|s| s.to_string()),
active_class: cx.props.active_class.map(|s| s.to_string()),
initial_url: cx.props.initial_url.clone(),
initial_url: cx.props.initial_url.clone().flatten(),
},
))
});

View file

@ -3,7 +3,7 @@ use crate::cache::StringCache;
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn};
use std::collections::HashMap;
use std::fmt::Write;
use std::rc::Rc;
use std::sync::Arc;
/// A virtualdom renderer that caches the templates it has seen for faster rendering
#[derive(Default)]
@ -25,7 +25,7 @@ pub struct Renderer {
pub skip_components: bool,
/// A cache of templates that have been rendered
template_cache: HashMap<&'static str, Rc<StringCache>>,
template_cache: HashMap<&'static str, Arc<StringCache>>,
}
impl Renderer {
@ -67,7 +67,7 @@ impl Renderer {
let entry = self
.template_cache
.entry(template.template.get().name)
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
.or_insert_with(|| Arc::new(StringCache::from_template(template).unwrap()))
.clone();
let mut inner_html = None;

View file

@ -254,7 +254,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
res = rx.try_next().transpose().unwrap().ok();
}
// Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks
// Todo: This is currently disabled because it has a negative impact on response times for events but it could be re-enabled for tasks
// Jank free rendering
//
// 1. wait for the browser to give us "idle" time