create fullstack hello world example

This commit is contained in:
Evan Almloff 2023-03-30 10:34:13 -05:00
parent 939e75541e
commit fdc8ebd1b1
11 changed files with 312 additions and 117 deletions

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

@ -0,0 +1 @@
target

View file

@ -1,5 +1,5 @@
[package] [package]
name = "server" name = "dioxus-server"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
@ -7,28 +7,37 @@ edition = "2021"
[dependencies] [dependencies]
server_fn = { version = "0.2.4", features = ["stable"] } server_fn = { version = "0.2.4", features = ["stable"] }
server_macro = { path = "server_macro" }
# warp # warp
warp = { version = "0.3.3", optional = true } warp = { version = "0.3.3", optional = true }
# axum # axum
axum = { version = "0.6.1", optional = true, features = ["ws"] } axum = { version = "0.6.1", optional = true, features = ["ws"] }
tower-http = { version = "0.4.0", optional = true, features = ["fs"] }
hyper = { version = "0.14.25", optional = true }
# salvo # salvo
salvo = { version = "0.37.7", optional = true, features = ["ws"] } salvo = { version = "0.37.7", optional = true, features = ["ws"] }
serde = "1.0.159" serde = "1.0.159"
dioxus = { path = "../dioxus", version = "^0.3.0" } dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true }
log = "0.4.17" log = "0.4.17"
once_cell = "1.17.1" once_cell = "1.17.1"
thiserror = "1.0.40" thiserror = "1.0.40"
hyper = "0.14.25" tokio = { version = "1.27.0", features = ["full"], optional = true }
tokio = { version = "1.27.0", features = ["full"] }
[features] [features]
default = ["axum", "ssr"] default = []
warp = ["dep:warp"] warp = ["dep:warp"]
axum = ["dep:axum"] axum = ["dep:axum", "tower-http", "hyper"]
salvo = ["dep:salvo"] salvo = ["dep:salvo"]
ssr = ["server_fn/ssr"] ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"]
[workspace]
members = [
"server_macro",
"examples/hello-world",
]

View file

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

View file

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

View file

@ -0,0 +1,70 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_server::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
#[cfg(feature = "ssr")]
{
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(
"Hello, world!",
"hello-world",
None,
None,
"",
app,
)
.into_make_service(),
)
.await
.unwrap();
});
}
}
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

@ -6,3 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
quote = "1.0.26"
server_fn_macro = { version = "0.2.4", features = ["stable"] }
syn = { version = "1", features = ["full"] }
[lib]
proc-macro = true

View file

@ -1,3 +1,7 @@
use proc_macro::TokenStream;
use quote::ToTokens;
use server_fn_macro::*;
/// Declares that a function is a [server function](leptos_server). This means that /// Declares that a function is a [server function](leptos_server). This means that
/// its body will only run on the server, i.e., when the `ssr` feature is enabled. /// its body will only run on the server, i.e., when the `ssr` feature is enabled.
/// ///
@ -52,14 +56,14 @@
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let context = ServerContext { let context = ServerContext {
ty: syn::parse_quote!(Scope), ty: syn::parse_quote!(DioxusServerContext),
path: syn::parse_quote!(::leptos::Scope), path: syn::parse_quote!(::dioxus_server::prelude::DioxusServerContext),
}; };
match server_macro_impl( match server_macro_impl(
args.into(), args.into(),
s.into(), s.into(),
Some(context), Some(context),
Some(syn::parse_quote!(::leptos::server_fn)), Some(syn::parse_quote!(::dioxus_server::prelude::server_fn)),
) { ) {
Err(e) => e.to_compile_error().into(), Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(), Ok(s) => s.to_token_stream().into(),

View file

@ -4,33 +4,68 @@ use axum::{
body::{self, Body, BoxBody, Full}, body::{self, Body, BoxBody, Full},
http::{HeaderMap, Request, Response, StatusCode}, http::{HeaderMap, Request, Response, StatusCode},
response::IntoResponse, response::IntoResponse,
routing::post, routing::{get, post},
Router, Router,
}; };
use dioxus_core::Component;
use server_fn::{Payload, ServerFunctionRegistry}; use server_fn::{Payload, ServerFunctionRegistry};
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use crate::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}; use crate::{
dioxus_ssr_html,
server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj},
};
trait DioxusRouterExt { pub trait DioxusRouterExt {
fn register_server_fns(self) -> Self; fn register_server_fns(self, server_fn_route: &'static str) -> Self;
fn serve_dioxus_application(
self,
title: &'static str,
application_name: &'static str,
base_path: Option<&'static str>,
head: Option<&'static str>,
server_fn_route: &'static str,
app: Component,
) -> Self;
} }
impl DioxusRouterExt for Router { impl DioxusRouterExt for Router {
fn register_server_fns(self) -> Self { fn register_server_fns(self, server_fn_route: &'static str) -> Self {
let mut router = self; let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() { for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}");
router = router.route( router = router.route(
server_fn_path, &full_route,
post(move |headers: HeaderMap, body: Request<Body>| async move { post(move |headers: HeaderMap, body: Request<Body>| async move {
server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await
// todo!()
}), }),
); );
} }
router router
} }
fn serve_dioxus_application(
self,
title: &'static str,
application_name: &'static str,
base_path: Option<&'static str>,
head: Option<&'static str>,
server_fn_route: &'static str,
app: Component,
) -> Self {
use tower_http::services::ServeDir;
// Serve the dist folder and the index.html file
let serve_dir = ServeDir::new("dist");
self.register_server_fns(server_fn_route)
.nest_service("/", serve_dir)
.fallback_service(get(move || {
let rendered = dioxus_ssr_html(title, application_name, base_path, head, app);
async move { Full::from(rendered) }
}))
}
} }
async fn server_fn_handler( async fn server_fn_handler(

View file

@ -1,2 +1,2 @@
#[cfg(feature = "axum")] #[cfg(feature = "axum")]
mod axum_adapter; pub mod axum_adapter;

View file

@ -1,104 +1,54 @@
#[allow(unused)]
use dioxus_core::prelude::*;
mod adapters; mod adapters;
mod server_fn;
// #[server(ReadPosts, "api")] pub mod prelude {
// async fn testing(rx: i32) -> Result<u32, ServerFnError> { #[cfg(feature = "axum")]
// Ok(0) pub use crate::adapters::axum_adapter::*;
// } pub use crate::server_fn::{DioxusServerContext, ServerFn};
pub use server_fn::{self, ServerFn as _, ServerFnError};
pub struct DioxusServerContext {} pub use server_macro::*;
#[cfg(any(feature = "ssr", doc))]
type ServerFnTraitObj = server_fn::ServerFnTraitObj<DioxusServerContext>;
#[cfg(any(feature = "ssr", doc))]
static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
std::sync::Arc<
std::sync::RwLock<
std::collections::HashMap<&'static str, std::sync::Arc<ServerFnTraitObj>>,
>,
>,
> = 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>,
) -> 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, server_function);
// 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<std::sync::Arc<ServerFnTraitObj>> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).cloned())
}
/// 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))] #[cfg(feature = "ssr")]
/// Errors that can occur when registering a server function. fn dioxus_ssr_html(
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] title: &str,
pub enum ServerRegistrationFnError { application_name: &str,
/// The server function is already registered. base_path: Option<&str>,
#[error("The server function {0} is already registered")] head: Option<&str>,
AlreadyRegistered(String), app: Component,
/// The server function registry is poisoned. ) -> String {
#[error("The server function registry is poisoned: {0}")] let mut vdom = VirtualDom::new(app);
Poisoned(String), let _ = vdom.rebuild();
let renderered = dioxus_ssr::pre_render(&vdom);
let base_path = base_path.unwrap_or(".");
let head = head.unwrap_or_default();
format!(
r#"
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />
{head}
</head>
<body>
<div id="main">
{renderered}
</div>
<script type="module">
import init from "/{base_path}/assets/dioxus/{application_name}.js";
init("/{base_path}/assets/dioxus/{application_name}_bg.wasm").then(wasm => {{
if (wasm.__wbindgen_start == undefined) {{
wasm.main();
}}
}});
</script>
</body>
</html>"#
)
} }
/// 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.
///
/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering,
/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.)
///
/// 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_by_path].
///
/// Technically, the trait is implemented on a type that describes the server function's arguments.
pub trait ServerFn: server_fn::ServerFn<DioxusServerContext> {
/// Registers the server function, allowing the server 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

@ -0,0 +1,98 @@
pub struct DioxusServerContext {}
#[cfg(any(feature = "ssr", doc))]
pub type ServerFnTraitObj = server_fn::ServerFnTraitObj<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, std::sync::Arc<ServerFnTraitObj>>,
>,
>,
> = 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>,
) -> 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, server_function);
// 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<std::sync::Arc<ServerFnTraitObj>> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).cloned())
}
/// 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.
///
/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering,
/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.)
///
/// 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_by_path].
///
/// Technically, the trait is implemented on a type that describes the server function's arguments.
pub trait ServerFn: server_fn::ServerFn<DioxusServerContext> {
/// Registers the server function, allowing the server 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> {}