Dioxus HotDog 示例实战:一个可跑在 Web、桌面与移动端的 Fullstack 狗狗图片查看器
【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus
本文以仓库中的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.1DogView:随机加载狗狗图片
#[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.2Favorites:列出与删除收藏
#[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进程。
【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考