添加后端服务
Dioxus 是一个全栈框架,这意味着您可以无缝地同时构建前端和后端。
我们提供了一系列实用工具,如服务器Functions、服务器 Future和服务器State,供您集成到应用程序中。在本章中,我们将介绍如何使用服务器函数加载和保存状态到后端。如需深入了解全栈框架,请查阅专用的全栈指南。
启用 fullstack
在开始使用服务器函数之前,我们需要在 Cargo.toml 中启用 Dioxus 的“全栈”功能。
[dependencies]
dioxus = { version = "0.6.0", features = ["fullstack"] }
我们还需要在 Cargo.toml 文件中将server功能添加到应用程序的功能列表中,并移除默认的 Web 目标。
[features]
default = [] # <----- remove the default web target
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
server = ["dioxus/server"] # <----- add this additional target
如果您在创建应用时选择了“使用全栈?”提示中的“是”,那么您已经完成了此设置!
📣 遗憾的是,dx 无法自动刷新此更改,因此我们需要终止当前运行的 dx serve 进程并重新启动它。
现在,您需要使用手动平台运行 dx serve,即dx serve --platform web。请给应用程序一点时间重新构建,并确保在仪表盘中启用了“全栈”功能。
服务器函数:内联 RPC 系统
Dioxus 与 server_fn 仓库集成,为您的应用程序提供一个简单的内联通信系统。server_fn 仓库使您能够仅使用基本的 Rust 函数轻松构建应用程序的后端。服务器函数是带有 #[server] 属性的异步函数。
一个典型的服务器函数看起来像这样:
#[server]
async fn save_dog(image: String) -> Result<(), ServerFnError> {
Ok(())
}
每个服务器函数都是一个异步函数,它接受一些参数并返回一个 Result<(), ServerFnError>。当客户端调用服务器函数时,它会向服务器上的对应端点发送一个 HTTP 请求。服务器函数的参数会被序列化为 HTTP 请求的正文。因此,每个参数都必须可序列化。
在客户端,服务器函数会被展开为一个 reqwest 调用:
// on the client:
async fn save_dog(image: String) -> Result<(), ServerFnError> {
reqwest::Client::new()
.post("http://localhost:8080/api/save_dog")
.json(&image)
.send()
.await?;
Ok(())
}
在服务器端,服务器功能扩展为一个 Axum 处理程序:
// on the server:
struct SaveDogArgs {
image: String,
}
async fn save_dog(Json(args): Json<SaveDogArgs>) -> Result<(), ServerFnError> {
Ok(())
}
当调用 dioxus::launch 时,服务器功能会自动为您注册并配置为 Axum 路由器。
async fn launch(config: ServeConfig, app: fn() -> Element) {
// register server functions
let router = axum::Router::new().serve_dioxus_application(config, app);
// start server
let socket_addr = dioxus::cli_config::fullstack_address_or_localhost();
let listener = tokio::net::TcpListener::bind(socket_addr).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
截至 Dioxus 0.6 版本,我们仅支持 Axum 服务器框架。我们计划在未来开发更多服务器功能,但目前仅支持 Axum 以加快发布速度。
在某些情况下,dioxus::launch 函数可能无法满足您在服务器端的具体需求。您可以通过修改 main.rs 文件轻松切换到 Axum。dioxus::launch 函数还负责配置日志记录和读取环境变量,这些操作您需要自行处理。
fn main() {
#[cfg(feature = "server")]
tokio::runtime::Runtime::new()
.unwrap()
.block_on(launch_server());
#[cfg(not(feature = "server"))]
dioxus::launch(App);
}
#[cfg(feature = "server")]
async fn launch_server() {
// Connect to dioxus' logging infrastructure
dioxus::logger::initialize_default();
// Connect to the IP and PORT env vars passed by the Dioxus CLI (or your dockerfile)
let socket_addr = dioxus::cli_config::fullstack_address_or_localhost();
// Build a custom axum router
let router = axum::Router::new()
.serve_dioxus_application(ServeConfigBuilder::new(), App)
.into_make_service();
// And launch it!
let listener = tokio::net::TcpListener::bind(socket_addr).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
客户端/服务器分离
当Dioxus构建您的全栈应用程序时,实际上会创建两个独立的应用程序:服务器端和客户端。为了实现这一点,dx会将不同的功能传递给每个构建过程。
客户端使用 --feature web 进行构建
服务器端使用 --feature server 进行构建
在将服务器代码嵌入到我们的应用程序中时,我们需要特别注意哪些代码会被编译。服务器函数的主体部分仅设计为在服务器端执行,而非客户端。通过 server 功能配置的任何代码都不会出现在最终应用程序中。相反,未通过server功能配置的任何代码都将被包含在最终应用程序中。
// ❌ this will leak your DB_PASSWORD to your client app!
static DB_PASSWORD: &str = "1234";
#[server]
async fn DoThing() -> Result<(), ServerFnError> {
connect_to_db(DB_PASSWORD).await
// ...
}
不过,我们建议将仅用于服务器的代码放置在配置了server 功能的模块中。
// ✅ code in this module can only be accessed on the server
#[cfg(feature = "server")]
mod server_utils {
pub static DB_PASSWORD: &str = "1234";
}
除了server功能外,Dioxus 还期待类似web或desktop的客户端渲染功能。某些库(如 web-sys)仅在浏览器中运行时有效,因此请确保不要在服务器函数中或启动前运行特定的客户端代码。您可以将仅限客户端的代码放置在针对特定客户端目标功能(如web)的配置中。
fn main() {
// ❌ attempting to use web_sys on the server will panic!
let window = web_sys::window();
// ..
dioxus::launch(App);
}
依赖项管理
某些依赖项(如Tokio)仅在针对原生平台时才能正确编译。其他依赖项(如jni-sys)仅在特定平台上运行时才能正常工作。在这些情况下,您需要确保这些依赖项仅在启用特定功能时才进行编译。为此,我们可以在Cargo.toml文件中使用Rust的可选标志来管理依赖项。
[dependencies]
tokio = { version = "1", optional = true }
[features]
default = []
server = ["dep:tokio"]
最终,如果你的项目足够大,你可能希望将服务器功能提取到一个独立的 crate 中,以便在不同的应用程序中使用。我们将在工作区中创建一个服务器 crate:
├── Cargo.toml
└── crates
├── dashboard
├── marketplace
└── server
然后,我们会在应用中导入服务器功能,并禁用其server 功能。
[dependencies]
server = { workspace = true, default-features = false }
我们在此提供一份更详细的指南,介绍如何在服务器和客户端之间管理依赖项。
我们的HotDog服务器函数
重新审视我们的HotDog应用程序,让我们创建一个新的服务器函数,将我们最喜欢的狗保存到一个名为dogs.txt的文件中。在生产环境中,您会希望使用一个合适的数据库,如下一章所述,但目前我们使用一个简单的文件来进行测试。
// Expose a `save_dog` endpoint on our server that takes an "image" parameter
#[server]
async fn save_dog(image: String) -> Result<(), ServerFnError> {
use std::io::Write;
// Open the `dogs.txt` file in append-only mode, creating it if it doesn't exist;
let mut file = std::fs::OpenOptions::new()
.write(true)
.append(true)
.create(true)
.open("dogs.txt")
.unwrap();
// And then write a newline to it with the image url
file.write_fmt(format_args!("{image}\n"));
Ok(())
}
调用服务器函数
现在,在我们的客户端代码中,我们可以实际调用服务器函数。
fn DogView() -> Element {
let mut img_src = use_resource(snipped!());
// ...
rsx! {
// ...
div { id: "buttons",
// ...
button {
id: "save",
onclick: move |_| async move {
let current = img_src.cloned().unwrap();
img_src.restart();
_ = save_dog(current).await;
},
"save!"
}
}
}
}
}
哇,我们的应用程序真的越来越完善了!
服务器功能非常强大,甚至可以使用服务器端渲染。如需更多信息,请查阅完整的全栈指南。