首页
/ Dioxus HotDog 示例实战:一个可跑在 Web、桌面与移动端的 Fullstack 狗狗图片查看器

Dioxus HotDog 示例实战:一个可跑在 Web、桌面与移动端的 Fullstack 狗狗图片查看器

2026-09-05 19:48:50作者:柏廷章Berta

本文以仓库中的 examples/01-app-demos/hotdog 目录为例,讲解 Dioxus 官方教程演示应用 HotDog 的完整实现:从 dx serve --platform 多端运行方式,到前端组件、服务端函数(server functions)、内存 SQLite 存储,再到基于 Dockerfile 与 Fly.io 的生产部署配置。读完后你可以掌握 Dioxus fullstack 模式下"一份代码、前后端同仓"的典型项目结构与运行、部署方式。

HotDog 演示应用截图

一、HotDog 是什么,以及如何运行

HotDog 是 Dioxus 团队为新教程准备的演示应用("Hot diggity dog!")。它的功能很简单但麻雀虽小五脏俱全:

  • 打开主页面,通过外部狗狗图片 API 随机加载一只狗的头像;
  • 点击 "skip" 换一只,点击 "save!" 把当前图片保存为收藏;
  • 收藏页面可以查看最近的收藏并支持删除。

按照 README 的说明,运行方式非常直接:先进入该目录,然后用 dx 命令为任意目标平台启动开发服务器:

# 先切换到示例目录
cd dioxus/hotdog

# 任选其一
dx serve --platform web
dx serve --platform desktop
dx serve --platform ios
dx serve --platform android

一条 dx serve 命令即可在 Web、桌面、iOS、Android 四个平台上跑起来,这正是 Dioxus 的多端目标:同一份源码编译到不同渲染后端。

二、项目结构与关键配置

该示例的目录非常精简,是学习 fullstack 项目的理想起点:

Cargo.toml 中的依赖与特性划分体现了 Dioxus fullstack 的典型写法:

[dependencies]
dioxus = { workspace = true, features = ["fullstack", "router"] }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
rusqlite = { version = "0.32.0", optional = true, features = ["bundled"] } # Bundle SQLite so Windows/MSVC builds do not require an external sqlite3.lib.
anyhow = { workspace = true }

[features]
default = ["web", "server"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
native = ["dioxus/native"]
mobile = ["dioxus/mobile"]
server = ["dioxus/server", "dep:rusqlite"]
production = []

几个值得注意的点:

  1. fullstack 特性:让前端与后端运行在同一项目里,后端接口以"服务端函数"的形式暴露给前端调用。
  2. server 特性:同时激活 dioxus/server 并引入 rusqlite 依赖。由于 rusqlite 是 optional 依赖,非服务端构建(如纯浏览器端)不会编译 SQLite,保持产物轻量;注释也说明了使用 bundled 特性是为了让 Windows/MSVC 构建无需外部 sqlite3.lib
  3. production 特性:一个空特性开关,仅用于标记生产构建,入口代码会根据它决定服务器地址(见后文)。

Dioxus.toml 则声明了应用名与打包标识:

[application]
name = "hot_dog"

[bundle]
identifier = "com.dioxuslabs"
publisher = "Dioxus Labs"

identifier 是移动端/桌面端打包时使用的 Bundle Identifier。

三、应用入口:路由与服务器地址

main.rs 完整展示了 fullstack 应用的入口形态:

mod backend;
mod frontend;

use dioxus::prelude::*;
use frontend::*;

#[derive(Routable, PartialEq, Clone)]
enum Route {
    #[layout(NavBar)]
    #[route("/")]
    DogView,

    #[route("/favorites")]
    Favorites,
}

fn main() {
    // only in production should we set the URL, otherwise let `dx` do the work
    #[cfg(all(not(feature = "server"), feature = "production"))]
    dioxus::fullstack::set_server_url("https://hot-dog.fly.dev");

    dioxus::launch(app);
}

fn app() -> Element {
    rsx! {
        Stylesheet { href: asset!("/assets/main.css") }
        Router::<Route> {}
    }
}
  • Route 枚举通过 #[derive(Routable)] 派生路由信息,两个页面分别是 DogView/)与 Favorites/favorites);#[layout(NavBar)] 指定 NavBar 作为布局组件包裹所有路由页面。
  • app() 里用 Stylesheet 挂载 asset!("/assets/main.css")asset! 是 Dioxus 的资产编译宏,由构建系统在编译期把资产打包并生成解析代码,因此无需在运行时手动拷贝静态文件。
  • main() 中有一段条件编译:只有在非服务端not(feature = "server"))且生产构建feature = "production")时才调用 set_server_url 把后端地址固定为部署好的 Fly.io 地址;开发模式下则由 dx 工具自动注入正确的本地服务器地址。set_server_url 的实现在 client.rs,它决定了前端发起服务端函数请求时的目标 URL。

四、前端页面:DogViewFavorites

前端逻辑全部在 frontend.rs 中,核心是 use_loader 这一响应式加载钩子——它在首次挂载(或 restart() 被调用时)执行一个异步闭包,返回 Result,成功值可以直接当 Signal 读取,失败则进入 Suspense 的错误状态。

4.1 DogView:随机加载狗狗图片

#[component]
pub fn DogView() -> Element {
    let mut img_src = use_loader(|| async move {
        #[derive(Deserialize, Serialize, Debug, PartialEq)]
        struct DogApi {
            message: String,
        }
        let json = reqwest::get("https://dog.ceo/api/breeds/image/random")
            .await?
            .json::<DogApi>()
            .await?;
        let url = json.message;

        dioxus::Ok(url)
    })?;

    rsx! {
        div { id: "dogview",
            img { id: "dogimg", src: "{img_src}" }
        }
        div { id: "buttons",
            button {
                id: "skip",
                onclick: move |_| img_src.restart(),
                "skip"
            }
            button {
                id: "save",
                onclick: move |_| async move { _ = save_dog(img_src()).await },
                "save!"
            }
        }
    }
}
  • 加载器直接在前端用 reqwest 请求随机狗狗图片接口,serde 反序列化出图片 URL;
  • "skip" 按钮调用 img_src.restart(),让 loader 重新执行——这是 use_loader 提供的"重新加载"能力;
  • "save!" 按钮则跨端调用服务端函数 save_dog(在 backend.rs 中定义),保存成功后数据留在服务端。

NavBar 组件(同一文件)则是路由的布局壳:

#[component]
pub fn NavBar() -> Element {
    rsx! {
        div { id: "title",
            span {}
            Link { to: Route::DogView, h1 { "🌭 HotDog! " } }
            Link { to: Route::Favorites, id: "heart", "♥️" }
        }
        Outlet::<Route> {}
    }
}

两个 Link 分别指向 Route::DogViewRoute::FavoritesOutlet::<Route> 是 Dioxus 路由的占位出口,当前匹配到的页面会渲染在这里。

4.2 Favorites:列出与删除收藏

#[component]
pub fn Favorites() -> Element {
    let mut favorites = use_loader(list_dogs)?;

    rsx! {
        div { id: "favorites",
            for (id , url) in favorites.cloned() {
                div { class: "favorite-dog", key: "{id}",
                    img { src: "{url}" }
                    button {
                        onclick: move |_| async move {
                            _ = remove_dog(id).await;
                            favorites.restart();
                        },
                        "❌"
                    }
                }
            }
        }
    }
}

注意这里 use_loader(list_dogs) 直接传入了服务端函数本身:前端把 list_dogs 当作普通异步闭包来 await,Dioxus fullstack 会在底层把它转换为一次 HTTP 请求发到服务端。删除时调用 remove_dog(id) 成功后再 favorites.restart() 刷新列表——"乐观地调用、然后重新加载"是这个示例采用的最简同步策略。

五、服务端函数:属性宏路由 + 线程本地 SQLite

backend.rs 是整个示例最有信息量的文件,展示了 Dioxus 服务端函数的完整写法:

use anyhow::Result;
use dioxus::prelude::*;

#[cfg(feature = "server")]
thread_local! {
    static DB: std::sync::LazyLock<rusqlite::Connection> = std::sync::LazyLock::new(|| {
        let conn = rusqlite::Connection::open(":memory:").expect("Failed to open database");

        conn.execute_batch(
            "CREATE TABLE IF NOT EXISTS dogs (
                id INTEGER PRIMARY KEY,
                url TEXT NOT NULL
            );",
        )
        .unwrap();

        conn
    });
}

#[get("/api/dogs")]
pub async fn list_dogs() -> Result<Vec<(usize, String)>> {
    DB.with(|db| {
        Ok(db
            .prepare("SELECT id, url FROM dogs ORDER BY id DESC LIMIT 10")?
            .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
            .collect::<Result<Vec<(usize, String)>, rusqlite::Error>>()?)
    })
}

#[delete("/api/dogs/{id}")]
pub async fn remove_dog(id: usize) -> Result<()> {
    DB.with(|db| db.execute("DELETE FROM dogs WHERE id = ?1", [id]))?;
    Ok(())
}

#[post("/api/dogs")]
pub async fn save_dog(image: String) -> Result<()> {
    DB.with(|db| db.execute("INSERT INTO dogs (url) VALUES (?1)", [&image]))?;
    Ok(())
}

实现要点:

  1. #[get] / #[post] / #[delete] 属性宏:把普通 async 函数声明为服务端函数,路径即 REST 风格接口(GET /api/dogsPOST /api/dogsDELETE /api/dogs/{id})。前端直接以函数引用(list_dogssave_dogremove_dog)调用它们,无需手写 HTTP 客户端;remove_dog(id: usize) 的参数从路径 {id} 中自动解析。
  2. #[cfg(feature = "server")] 门控thread_local! 数据库连接以及全部服务端逻辑只在开启 server 特性的构建中存在。结合 Cargo.toml 中 server = ["dioxus/server", "dep:rusqlite"],可以确认:纯客户端构建不会链接任何数据库代码。
  3. 存储选型:使用 rusqlite 打开 :memory: 内存数据库,并用 thread_local + LazyLock 让每个线程持有独立的 Connection(SQLite 连接不是 Sync 的,线程本地是 rusqlite 的典型用法),表结构就一张 dogs(id INTEGER PRIMARY KEY, url TEXT)list_dogsid DESC LIMIT 10 只返回最近 10 条收藏。

从源码结构看,这个示例刻意选择内存数据库是为了教学上的"零配置"——数据随进程消失。若要持久化,只需把 :memory: 换成文件路径;仓库中附带的 fly.toml 也确实声明了一个挂载卷(source = "hotdogdb" 挂载到 /usr/local/app/hotdogdb),说明部署方保留了挂持久卷的位置。

六、样式:单文件 CSS 的简单布局

assets/main.css 用不到 150 行 CSS 完成了全部视觉:深色背景(#0e0e0e)、Flex 布局的 #dogview 居中展示图片、#buttons 横排两个大按钮(#skip 灰色、#save 绿色)、收藏页 #favorites-container 的换行流式布局,以及一个小巧的交互细节——收藏图片上的删除按钮默认 display: none,只有 .favorite-dog:hover button 才显示,即"悬停才露出删除按钮"。

七、生产构建与部署:Dockerfile + Fly.io

该示例同时给出了完整的生产部署链路。Dockerfile 采用多阶段构建:

FROM rust:1 AS chef
RUN cargo install cargo-chef
WORKDIR /app

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN curl -L --proto '=https' --tlsv1.2 -sSf <dx 安装脚本> | bash
RUN dx bundle --platform web --features production

FROM chef AS runtime
COPY --from=builder /app/target/dx/hotdog/release/web/ /usr/local/app

ENV PORT=8080
ENV IP=0.0.0.0
EXPOSE 8080

WORKDIR /usr/local/app
ENTRYPOINT [ "/usr/local/app/server" ]

(上面 <dx 安装脚本> 处原文是从 DioxusLabs 官方仓库拉取的 install.sh 安装脚本,此处为避免外部链接做脱敏表述。)

关键步骤解读:

  • cargo-chef 分阶段planner 阶段先生成依赖清单,builder 阶段先编译依赖再拷贝源码,充分利用 Docker 层缓存加速 Rust 构建;
  • dx bundle --platform web --features production:这是生产构建的核心命令。production 特性触发 main.rs 中的条件编译,把服务端地址固定为已部署的 https://hot-dog.fly.dev;产物落在 target/dx/hotdog/release/web/
  • 运行阶段:直接把整个 web 产物目录拷进镜像,ENTRYPOINT 指向其中的 server 可执行文件——即 Dioxus fullstack 的 axum 服务端进程,它会同时托管前端静态资源与 /api/dogs 服务端函数接口,监听 0.0.0.0:8080

fly.toml 是配套的 Fly.io 部署描述:

app = 'hot-dog'
primary_region = 'sjc'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

[mounts]
  source = "hotdogdb"
  destination = "/usr/local/app/hotdogdb"

internal_port = 8080 与 Dockerfile 的 ENV PORT=8080 对应;min_machines_running = 0 + 自动启停意味着闲置时可零实例运行;mounts 声明的 hotdogdb 卷则与 SQLite 数据库的落盘位置预留对应。这也解释了 main.rsset_server_url("https://hot-dog.fly.dev") 这一地址的由来。

八、小结:从示例能学到的 fullstack 模式

HotDog 示例用最小代码量串起了 Dioxus fullstack 的几个核心机制:

机制 在示例中的体现 参考文件
多端运行 dx serve --platform web/desktop/ios/android README
特性驱动的前后端分离编译 server / production 特性门控 Cargo.toml
路由与布局 Routable 派生、LinkOutlet main.rsfrontend.rs
响应式异步加载 use_loaderrestart() frontend.rs
服务端函数 #[get]/#[post]/#[delete] 属性宏 backend.rs
资产编译 asset!("/assets/main.css") + Stylesheet main.rsmain.css
生产部署 dx bundle + 多阶段 Docker + Fly.io Dockerfilefly.toml

如果你想在自己项目里复刻这套结构,最小步骤是:开启 dioxusfullstack 特性;用 #[get]/#[post] 等属性宏定义服务端函数;前端用 use_loader 直接 await 这些函数;开发时 dx serve --platform web 即可联调,生产时用 dx bundle --platform web --features production 生成可独立部署的 server 进程。

登录后查看全文
热门项目推荐
相关项目推荐