Dioxus 的后端服务

370 阅读6分钟

添加后端服务

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 文件轻松切换到 Axumdioxus::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 还期待类似webdesktop的客户端渲染功能。某些库(如 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!"
            }
        }
    }
}
}

哇,我们的应用程序真的越来越完善了!

dog-save-serverfn-2bff01ff3208c788.gif

服务器功能非常强大,甚至可以使用服务器端渲染。如需更多信息,请查阅完整的全栈指南