Dioxus HotDog 示例实战:一个可跑在 Web、桌面与移动端的 Fullstack 狗狗图片查看器
本文以仓库中的 examples/01-app-demos/hotdog 目录为例,讲解 Dioxus 官方教程演示应用 HotDog 的完整实现:从 dx serve --platform 多端运行方式,到前端组件、服务端函数(server functions)、内存 SQLite 存储,再到基于 Dockerfile 与 Fly.io 的生产部署配置。读完后你可以掌握 Dioxus fullstack 模式下"一份代码、前后端同仓"的典型项目结构与运行、部署方式。
一、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 项目的理想起点:
- main.rs:应用入口,定义路由枚举并
dioxus::launch(app); - frontend.rs:前端页面组件(
DogView、Favorites、NavBar); - backend.rs:服务端函数,负责读写收藏数据;
- assets/main.css:全局样式;
- Cargo.toml 与 Dioxus.toml:依赖与平台特性、打包标识配置;
- Dockerfile 与 fly.toml:生产部署配置。
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 = []
几个值得注意的点:
fullstack特性:让前端与后端运行在同一项目里,后端接口以"服务端函数"的形式暴露给前端调用。server特性:同时激活dioxus/server并引入rusqlite依赖。由于rusqlite是 optional 依赖,非服务端构建(如纯浏览器端)不会编译 SQLite,保持产物轻量;注释也说明了使用bundled特性是为了让 Windows/MSVC 构建无需外部sqlite3.lib。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。
四、前端页面:DogView 与 Favorites
前端逻辑全部在 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::DogView 与 Route::Favorites,Outlet::<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(())
}
实现要点:
#[get]/#[post]/#[delete]属性宏:把普通 async 函数声明为服务端函数,路径即 REST 风格接口(GET /api/dogs、POST /api/dogs、DELETE /api/dogs/{id})。前端直接以函数引用(list_dogs、save_dog、remove_dog)调用它们,无需手写 HTTP 客户端;remove_dog(id: usize)的参数从路径{id}中自动解析。#[cfg(feature = "server")]门控:thread_local!数据库连接以及全部服务端逻辑只在开启server特性的构建中存在。结合 Cargo.toml 中server = ["dioxus/server", "dep:rusqlite"],可以确认:纯客户端构建不会链接任何数据库代码。- 存储选型:使用
rusqlite打开:memory:内存数据库,并用thread_local+LazyLock让每个线程持有独立的Connection(SQLite 连接不是Sync的,线程本地是 rusqlite 的典型用法),表结构就一张dogs(id INTEGER PRIMARY KEY, url TEXT)。list_dogs按id 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.rs 里 set_server_url("https://hot-dog.fly.dev") 这一地址的由来。
八、小结:从示例能学到的 fullstack 模式
HotDog 示例用最小代码量串起了 Dioxus fullstack 的几个核心机制:
| 机制 | 在示例中的体现 | 参考文件 |
|---|---|---|
| 多端运行 | dx serve --platform web/desktop/ios/android |
README |
| 特性驱动的前后端分离编译 | server / production 特性门控 |
Cargo.toml |
| 路由与布局 | Routable 派生、Link、Outlet |
main.rs、frontend.rs |
| 响应式异步加载 | use_loader、restart() |
frontend.rs |
| 服务端函数 | #[get]/#[post]/#[delete] 属性宏 |
backend.rs |
| 资产编译 | asset!("/assets/main.css") + Stylesheet |
main.rs、main.css |
| 生产部署 | dx bundle + 多阶段 Docker + Fly.io |
Dockerfile、fly.toml |
如果你想在自己项目里复刻这套结构,最小步骤是:开启 dioxus 的 fullstack 特性;用 #[get]/#[post] 等属性宏定义服务端函数;前端用 use_loader 直接 await 这些函数;开发时 dx serve --platform web 即可联调,生产时用 dx bundle --platform web --features production 生成可独立部署的 server 进程。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
