- 支持试读
开启 dioxus 之旅
本章将讨论 dioxus 的安装、项目创建、运行和构建。 - 支持试读
dioxus 组件与 rsx
本章将开始第一个案例:计数器的编写。首先,我们需要学习 dioxus 组件的编写和 rsx 语法。 - 支持试读
计数器
本章我们将开始编写计数器。你将学习到:事件处理、状态管理、组件通讯、集成 Tailwind CSS 等知识。 - 支持试读
博客
本章我们将实现博客,你将学习到:dioxus 路由、获取远程数据、条件渲染、列表渲染等知识。 全局状态
本章我们开始最后一个案例:阅后即焚的开发。在真正进行开发之前,我们还需要掌握一些基础知识,本章我们将学习 dioxus 的全局状态。布局与嵌套路由
本章我们将讨论 dioxus 的布局与嵌套路由。表单处理
本章我们将学习 dioxus 的表单处理。和 React 类似,dioxus 也分为受控表单和非受控表单。阅后即焚前台UI
本章我们开始编写阅后即焚的前台UI。- 支持试读
阅后即焚API
本章我们使用 AXUM 开发阅后即焚的前台 API。 整合阅后即焚前端和API
本章将对阅后即焚前端和API进行整合。- 支持试读
编译和部署
本章我们将讨论 dioxus web 的编译和部署到 NGINX。 【加餐】dioxus 服务端渲染
本章使用前文的『博客』中的用户数据为案例,来讲解 dioxus 服务端渲染。
博客
- 5
- 2025-04-30 21:19:14
本章我们将实现博客,你将学习到:dioxus 路由、获取远程数据、条件渲染、列表渲染等知识。
本案例将使用 JSON PLACHOLDER 提供的 API,使用 dioxus 实现一个简单的博客。
创建并初始化项目
创建项目:
dx new blog
然后,按照上一章的讲解,进行文件清理,并集成 Tailwind CSS
// src/main.rs
use dioxus::prelude::*;
const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/main.css");
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
div { class:"text-red-600 text-xl", "Hello, 世界" }
}
}
路由
为了启用 dioxus 的路由功能,我们需要在 Cargo.toml
的 dioxus 依赖中,加上 router
features:
cargo add dioxus --features router
也可以手动编辑 Cargo.toml
[dependencies]
dioxus = { version = "0.6.0", features = ["router"] }
定义路由枚举
dioxus 的路由需要由一个带有 Routable
宏的枚举来定义;每个具体的路由使用 #[route(path)]
来声明。
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Home {},
#[route("/about")]
About {},
}
我们定义了2个路由:
/
:首页,对应Home
组件/about
:关于,对应About
组件
这两个组件很简单:
#[component]
fn Home() -> Element {
rsx! {
div { class: "text-red-600 text-xl", "首页" }
}
}
#[component]
fn About() -> Element {
rsx! {
div { class: "text-blue-600 text-xl", "关于" }
}
}
开启路由
定义好了路由,我们需要在 App
组件中开启路由:
#[component]
fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
Router::<Route> {}
}
}
Router::<Route>{}
开启路由。Router
是一个泛型函数,我们把定义的 Route
枚举作为泛型参数进行调用。
动态路由
dioxus 还支持动态路由:
#[derive(Routable, Clone)]
enum Route {
...
#[route("/detail/:id")]
Detail { id: i32 },
}
#[component]
fn Detail(id: i32) -> Element {
rsx! {
div { class: "text-cyan-600 text-xl", "#{id} 的详情" }
}
}
嵌套路由
dioxus 也支持嵌套路由,我们将在第3个案例中进行讲解。
路由链接
dioxus 提供了 Link
组件来实现路由链接:
#[component]
fn Detail(id: i32) -> Element {
rsx! {
div { class: "text-cyan-600 text-xl", "#{id} 的详情" }
ul {
li {
Link { to: Route::Home {}, "首页" }
}
}
}
}
获取远程数据
cargo add reqwest --features json
cargo add serde --features derive
下面,我们尝试获取 https://jsonplaceholder.typicode.com/posts/1
的数据:
#[derive(serde::Deserialize, Debug, Clone)]
pub struct Post {
#[serde(rename = "userId")]
pub user_id: u32,
pub id: u32,
pub title: String,
pub body: String,
}
#[component]
fn BlogPost() -> Element {
let mut post = use_signal(|| None::<Post>);
let fetch_post = move |_| async move {
let resp = reqwest::get("https://jsonplaceholder.typicode.com/posts/1")
.await
.unwrap()
.json::<Post>()
.await
.unwrap();
post.set(Some(resp));
};
if let Some(post) = post.cloned() {
rsx! {
div { "ID: {post.id}" }
div { "标题:{post.title}" }
}
} else {
rsx! {
button {
class: "px-3 py-1 bg-gray-600 text-white",
onclick: fetch_post,
"获取数据"
}
}
}
}
以上方式美中不足的是,为了将状态渲染到页面中,对该状态进行了 Clone
操作,如果状态维护的是一个很大的结构体,将会浪费资源。我们可以使用智能指针改进:
#[derive(serde::Deserialize, Debug)]
pub struct Post {
#[serde(rename = "userId")]
pub user_id: u32,
pub id: u32,
pub title: String,
pub body: String,
}
#[component]
fn BlogPost() -> Element {
let mut post = use_signal(|| None::<Rc<Post>>);
let fetch_post = move |_| async move {
let resp = reqwest::get("https://jsonplaceholder.typicode.com/posts/1")
.await
.unwrap()
.json::<Post>()
.await
.unwrap();
post.set(Some(Rc::new(resp)));
};
if let Some(post) = post.cloned() {
rsx! {
div { "ID: {post.id}" }
div { "标题:{post.title}" }
}
} else {
rsx! {
button {
class: "px-3 py-1 bg-gray-600 text-white",
onclick: fetch_post,
"获取数据"
}
}
}
}
- 结构体
Post
不实现Clone
- 状态不是简单的
Option<Post>
,而是Rc<Option<Post>>
- 由于使用了
Rc
智能指针,即使在渲染页面时调用了Clone
,也只是简单的增加了Rc
的引用次数,而不是克隆整个结构体
使用 use_resource()
在获取远程资源的场景下,dioxus 提供比 use_signal
更优的 hook:use_resource()
。我们用 use_resource()
改进一下:
我们简单了解一下 use_resource()
:
- 它会自动执行作为其参数的那个闭包
- 它的结果中包含了
Option
- 使用
restart()
方法,可以重新执行那个闭包
上面的代码又用到了 Clone,其实 use_resource()
提供了一种方式来清除这个 Clone 操作:
#[component]
fn BlogPost() -> Element {
let mut post = use_resource(|| async move {
let resp = reqwest::get("https://jsonplaceholder.typicode.com/posts/1")
.await
.unwrap()
.json::<Post>()
.await
.unwrap();
resp
});
match &*post.read_unchecked() {
Some(p) => rsx! {
div { "ID: {p.id}" }
div { "标题:{p.title}" }
button {
class: "px-3 py-1 bg-gray-600 text-white",
onclick: move |_| post.restart(),
"获取数据"
}
},
None => rsx! {
div { "Loading..." }
},
}
}
use_resource()
其实是一个 Future
,而 read_unchecked()
更为优雅的用法是根据这个 Future
的结果进行处理:
#[component]
fn BlogPost() -> Element {
let mut post = use_resource(|| async move {
reqwest::get("https://jsonplaceholder.typicode.com/posts/1")
.await
.unwrap()
.json::<Post>()
.await
});
match &*post.read_unchecked() {
Some(Ok(p)) => rsx! {
div { "ID: {p.id}" }
div { "标题:{p.title}" }
button {
class: "px-3 py-1 bg-gray-600 text-white",
onclick: move |_| post.restart(),
"获取数据"
}
},
Some(Err(e)) => rsx! {
div { "Error: {e}" }
},
None => rsx! {
div { "Loading..." }
},
}
}
有了这些基础知识,我们可以开始编写博客程序了。
模型定义
本案例主要有2个模型,分别是博文 Post
和用户 User
:
博文模型:
#[derive(Serialize, Deserialize, Debug)]
pub struct Post {
#[serde(rename = "userId")]
pub user_id: u32,
pub id: u32,
pub title: String,
pub body: String,
}
用户模型:
#[derive(Serialize, Deserialize, Debug)]
pub struct Geo {
pub lat: String,
pub lng: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Address {
pub street: String,
pub suite: String,
pub city: String,
pub zipcode: String,
pub geo: Geo,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Company {
pub name: String,
#[serde(rename = "catchPhrase")]
pub catch_phrase: String,
pub bs: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
pub id: u32,
pub name: String,
pub username: String,
pub email: String,
pub address: Address,
pub phone: String,
pub website: String,
pub company: Company,
}
博客列表
#[component]
pub fn List() -> Element {
let posts = use_resource(|| async move {
reqwest::get("https://jsonplaceholder.typicode.com/posts")
.await
.unwrap()
.json::<Vec<model::Post>>()
.await
});
match &*posts.read_unchecked() {
Some(Ok(ps)) => rsx! {
ul { class: "list-disc list-inside space-y-2",
for post in ps {
li {
a {
class: "hover:text-red-600 capitalize",
href: format!("/post/{}", post.id),
"{post.title}"
}
}
}
}
},
Some(Err(e)) => rsx! {
div { "获取博文数据出错:{e}" }
},
None => rsx! {
Loading {}
},
}
}
- 通过
use_resource
获取远程 API 数据 - 匹配
read_unchecked()
的结果,根据不同结果渲染不同UI - dioxus 支持两种渲染列表的方式
- 使用
for ... in
- 使用
map()
- 使用
博客详情
#[component]
pub fn Detail(id: u32) -> Element {
let post = use_resource(move || async move {
reqwest::get(format!("https://jsonplaceholder.typicode.com/posts/{}", id))
.await
.unwrap()
.json::<model::Post>()
.await
});
match &*post.read_unchecked() {
Some(Ok(p)) => rsx! {
div { class: "space-y-4",
h1 { class: "text-xl font-semibold capitalize", "{p.title}" }
p { class: "text-lg", "{p.body}" }
}
},
Some(Err(e)) => rsx! {
div { "获取博文数据出错:{e}" }
},
None => rsx! {
Loading {}
},
}
}
用户
用户模型我们已经定义好了,用户相关的页面,作为作业留给读者完成。
用户详情:https://jsonplaceholder.typicode.com/users/1
。最后的 1
为用户ID。
路由
use crate::component::post::{Detail as PostDetail, List as PostList};
use dioxus::prelude::*;
#[derive(Routable, Clone)]
pub enum Route {
#[route("/")]
PostList {},
#[route("/post/:id")]
PostDetail { id: u32 },
}
主组件
#[component]
fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
div { class: "bg-gray-100 min-h-screen",
header { class: "container mx-auto flex justify-start items-end gap-x-8 py-6",
a { class: "flex items-center gap-x-2", href: "/",
img { src: LOGO_IMG, class: "w-8 object-convert" }
h2 { class: "text-2xl font-bold", "博客" }
}
nav { class: "",
ul {
li {
a {
class: "text-xl hover:text-red-800",
href: "/",
"博文"
}
}
}
}
}
main { class: "container mx-auto p-6 bg-white", Router::<Route> {} }
}
}
}