rust 全栈应用框架dioxus server
接上一篇文章dioxus
全栈应用框架的基本使用,支持web、desktop、mobile
等平台。
可以先查看上一篇文章rust 全栈应用框架dioxus👈
既然是全栈框架,那肯定是得有后端服务的,之前创建的服务没有包含后端服务包,我们修改Cargo.toml
,增加后端服务包,
指定依赖dioxus
包含特性fullstack
dioxus = { version = "0.6.0", features = ['fullstack'] }
并且需要去掉当前默认的default
平台设置,增加server
服务端功能。之后需要重启项目,并且需要指定平台dx serve --platform web
[features]
default = ["web"] // 移除
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
server = ["dioxus/server"] // 增加
重新启动服务后,类似本地mock服务,可以像调用接口一样在前端组件逻辑中调用。
启动之后可以看到和仅前端服务不同的是多了一个fullstack
.这样我们可以开始编写服务端代码了。
以一个简单的记录信息的服务为例,保存用户输入的信息,并展示用户已经保存的信息数据。
内连服务RPC
内连服务功能函数定义和界面定义代码没有分离。通过#[server]
定义服务端功能函数,函数是异步async
的。
我们定义保存用户输入信息的函数,返回值为Result<(), ServerFnError>
,请求参数会自动被序列化,响应参数也必须可被序列化。
#[server]
async fn save_note(content:String) -> Result<(), ServerFnError> {Ok(())
}
在页面上通过点击事件,调用后端服务,dioxus
使用axum
来处理后端服务,前端则可以通过reqwest
库请求服务。内连服务则可以让我们直接在事件处理方法中调用服务端函数。
axusm
是一个web服务框架,集成了tokio
异步运行时;tower
构建客户端和服务端;hyper
http服务库。
定义好了服务端功能函数,在客户端通过点击事件进行调用。dioxus
会自动处理调用到的服务函数,注册服务,建立调用关系。
#[component]
fn App() -> Element {let mut content = use_signal(|| "".to_string());let handle_input = move |event: FormEvent| {content.set(event.value());};let handle_submit = move |_| async move {save_note(content()).await.unwrap();};rsx! {input { value:"{content}", oninput:handle_input }button { onclick:handle_submit,"submit" }}
}
用户输入点击提交,然后调用服务端函数save_note
保存信息。完善一下服务端功能,将输入的信息存储到当前目录文件中note.txt
。
#[server]
async fn save_note(content:String) -> Result<(), ServerFnError> {println!("received note {}", content);// 存储到当前目录文件中 note.txtstd::fs::write("note.txt", content).unwrap();Ok(())
}
运行测试dx serve --platform web
启动服务时,发现报错了
根据问题查询是因为在web平台要构建server
服务时,中间依赖的mio
库是一个服务端网络库,不能编译为WASM。web
端不能运行服务端程序,所以改换平台测试dx serve --platform desktop
改用desktop
平台,运行成功,按照功能测试了输入内容并点击保存,项目根目录下出现了文件note.txt
文件,内容正是我们输入的内容。
手动注册服务
通过#[server]
定义的服务端功能函数,在运行时会启动注册服务,这就限制了无法在不能运行服务端程序的平台上运行。比如不能在web
平台上执行,我们可以手动注册服务,并通过运行环境判断执行前端服务还是后端服务。
在自定义服务时,需要添加依赖axum \ tokio
,使用了optional
来标记依赖,这是因为某些依赖只在特定的平台中运行,然后在特定的平台特性中指定需要的依赖。
[dependencies]
axum = { version = "0.7.9", optional = true }
tokio = { version = "1.44.2", features = ["full"], optional = true }[features]
server = ['dioxus/server', "dep:axum", "dep:tokio"]
定义服务注册函数launch_server
,通过#[cfg(feature = "server")]
条件判断只有在features
为server
时才编译代码,这样在启动web
平台时不会自动将服务端代码进行编译,达到web和server分离的目的。
#[cfg(feature = "server")]
async fn launch_server() {// 获取到服务ip 端口// 这里依赖了`dioxus`的features cli_configlet addr = cli_config::fullstack_address_or_localhost();// 自定义axum 路由let router = axum::Router::new().serve_dioxus_application(ServeConfigBuilder::new(), App).into_make_service();// 监听端口let listener = tokio::net::TcpListener::bind(addr).await.unwrap();// 启动服务axum::serve(listener, router).await.unwrap();
}
dioxus::fullstack
提供了与axum
集成的服务能力。提供了serve_dioxus_application
方法,它提供完整的服务端渲染应用的能力。
注意:目前我使用的
dioxus
版本是0.6.3
,对应的axum
版本是0.7
。最新的axum
版本是0.8
,不支持serve_dioxus_application
,这可能会在dioxus
新版本0.7
中解决。
定义好了服务端运行函数,我们修改主函数main.rs
,通过条件编译#[cfg(feature = "server")]
只有在特性sever时执行我们定义的launch_server
,其它时执行前端运行函数dioxus::launch(App)
。
fn main() {#[cfg(feature = "server")]tokio::runtime::Runtime::new().unwrap().block_on(launch_server());#[cfg(not(feature = "server"))]dioxus::launch(App);
}
现在可以直接运行dx serve --platform web
,现在是服务端渲染,我们可以正常的访问前端页面,并且通过接口调用到了后端服务。
可以看到默认转换的服务API地址,前缀默认是api
,请求地址、类型等设置可以通过#[server]
参数进行设置,这里暂不涉及,需要的可以去查看文档。
在前后端混合开发时,注意一些服务端需要的静态变量,比如密码,数据库连接等,不能直接定义变量,通过条件编译#[cfg(feature = "server")]
进行处理。
分离服务
我们可以采用rust
工作区来管理项目,区分服务端server
和前端,然后前端又可以区分为web
、mobile
、desktop
,将公共的页面逻辑放在app
中,这样我们的目录就变成了
那么之前的区分server
的入口执行代码存放在server
目录中
use dioxus::prelude::*;// 不同平台的页面入口组件
use web::App;#[cfg(feature = "server")]
#[tokio::main]
async fn main() {let addr = dioxus::cli_config::fullstack_address_or_localhost();let router = axum::Router::new().serve_dioxus_application(ServeConfigBuilder::new(), App).into_make_service();let listener = tokio::net::TcpListener::bind(addr).await.unwrap();axum::serve(listener, router).await.unwrap();
}#[cfg(not(feature = "server"))]
fn main() {dioxus::launch(App);
}
这里还是依赖了dioxus
提供的服务端能力,也可以自己使用axum
自定义服务端功能,在调用#[server]
定义的服务端函数的地方改为传统接口请求方式。
将通用的组件,包括服务端函数等放在子包app
中,然后在不同的平台的引入并使用。
use dioxus::prelude::*;use app::App as BaseApp;#[component]
pub fn App() -> Element {rsx! {h2 { "Hello, web!" }BaseApp {}}
}
明确不同平台、不同能力划分的子包,可以更方便的管理,也能更好的针对不同平台进行定制化处理。
官方并没有给出一个标准示例,这方面还需要继续探索。
#[server]
可以将前后端写在一起,再区分模块是否不妥,还需实践。
路由
路由在业务开发中必不可少,dioxus
提供了特性router
支持路由配置,我们修改依赖增加特性支持
[workspace.dependencies]
dioxus = { version = "0.6.3", features = ["fullstack", "router"] }
diosux
提供了派生宏Routable
使得我们通过枚举定义路由:
#[derive(Routable, Clone, PartialEq)]
enum Route {#[route("/")]Home,
}
定义了默认导航路由地址/
渲染组件Home
,在需要渲染路由的地方使用Router::<Route> {}
来占位路由渲染。修改组件App
增加路由渲染占位:
#[component]
pub fn App() -> Element {rsx! {Router::<Route> {}}
}
我们定义Home
组件,默认可以展示来自不同平台的平台名称,在各个平台中通过use_context_provider
hook提供了平台名称。比如在web
平台中:
use dioxus::prelude::*;use app::App as BaseApp;#[component]
pub fn App() -> Element {use_context_provider(|| "dioxus-web".to_string());rsx! {BaseApp {}}
}
在Home
组件中获取并展示,上下文变量共享可以作为不同平台的环境变量来处理一些特定的逻辑。
#[component]
pub fn Home() -> Element {let platform_name: String = use_context();rsx! {h2{"{platform_name}"}}
}
我们将原来的输入信息保存的功能提取成一个组件Note
,并默认初始渲染这个组件,我们还希望Home
组件也能展示,也就是Home
组件是父组件,Note
组件是子组件。
通过Outlet::<Route> {}
将路由匹配的组件渲染到指定的位置,修改组件Home
use dioxus::prelude::*;use crate::Route;#[component]
pub fn Home() -> Element {let platform_name: String = use_context();rsx! {h2{"{platform_name}"}// 渲染子组件Outlet::<Route> {}}
}
路由/
默认渲染Note
,调整路由定义,通过#[layout]
定义组件嵌套关系,在父组件内部可通过Outlet
渲染匹配到的子组件。
#[derive(Routable, Clone, PartialEq)]
enum Route {#[layout(Home)]#[route("/")]AddNote,
}
上面默认路由/
渲染了Note
,在日常开发中/
路由渲染可能会发生改变,为了方便灵活配置,指定跳转到其他路由。新增一个路由/note
,用来访问Note
组件,然后路由/
重定向到/note
。
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
enum Route {#[redirect("/",|| Route::AddNote)]#[layout(Home)]#[route("/note")]AddNote,#[end_layout]
}
为了标识嵌套关系,使用#[rustfmt::skip]
保持手动缩进格式,使用#[redirect]
重定向路由,第一个参数指定路径;第二个闭包函数返回渲染的路由(已经定义了的路由)。
当前重定向目标路由时,浏览器的访问路径并不会由
/
更改为/note
动态路由
动态路由包括动态路径和查询参数。
动态路径就是通过:name
表示参数name
可以是任意值。
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
enum Route {#[route("/note/:id")]ViewNote {id:String},
}
查询参数则是在路径后面加上?
,后面跟:name
,多个参数用&
连接。
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
enum Route {#[route("/note?:name&:id")]ViewNote { name:String, id:String },
}
在传递参数时,需要按照参数顺序定义,比如name
字段必须在id
前面。{ id:String, name:String }
这样写是错的。
路由嵌套
通过#[layout]
实现组件嵌套。实现路由嵌套可以减少路径重复书写,通过#[nest]
标识上级路径
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
enum Route {#[redirect("/",|| Route::AddNote)]#[layout(Home)]#[nest("/note")]#[route("/")]AddNote,#[route("/view?:name&:id")]ViewNote { name:String,id:String },#[end_nest]#[end_layout]
}
路由嵌套中也可以使用动态路径,它可以将参数传递给所有子路由。
404页面
路由404页面,当匹配不到所有的路由定义时,渲染指定的页面,在路由配置最后新增路由兜底,通过:..segments
匹配所有路径段:
#[derive(Routable, Clone, PartialEq)]
enum Route {// ... routes#[route("/:..segments")]NotFound { segments: Vec<String> },
}
也可通过redirect
重定向到首页去,我们在初始默认/
渲染的Home
组件,可以在路由重定向,当匹配不到其他路由路径时,渲染Home
组件。
#[derive(Routable, Clone, PartialEq)]
enum Route {#[route("/")]#[redirect("/:..segments",|segments:Vec<String>| Route::Home {})]Home,
}
对于处理路由匹配不到的处理只能选择其中一个配置,不能同时设置。
对于:..routes
捕获剩余路由路径也可用于路由的嵌套路由中,比如#[route("/note/:..routes")]
路由导航
dioxus
提供了组件Link
用来跳转到指定路由。
#[component]
pub fn Home() -> Element {rsx! {Link { to:Route::AddNote {} , "Add Note" },Link { to:"https://www.baidu.com", "Baidu" },}
}
也支持直接跳转第三方链接。还可以通过navigator
全局函数获取到导航实例,通过方法手动跳转指定路由
push
跳转到指定路由replace
替换当前路由,路由历史丢失,不能回退。go
跳转到指定路由,路由历史保留,可以回退。go_back
返回上一级路由。go_forward
返回下一级路由。
#[component]
pub fn Home() -> Element {let router = navigator();rsx! {button {onclick: move |_| {router.push(Route::AddNote {});},"Add Note"}}
}
虽然功能上navigator
和Link
类似,但是对于外部链接navigator
并不保证跳转成功。
为了方便路由的前进、后退,dioxus
提供了全局组件GoBackButton
和GoForwardButton
直接使用,避免了通过点击事件处理函数手动跳转。
#[component]
pub fn Add() -> Element {rsx! {GoBackButton {"back"}}
}
连接数据库
现在能用的数据库很多了,这里找一个简单的数据库测试存储。不需要额外安装的嵌入式数据库,比如SQLite
安装依赖rusqlite
cargo add rusqlite --optional
新增一个db.rs
用于管理操作数据库,数据库操作只能在服务端运行, 我们需要使用#[cfg(feature = "server")]
#[cfg(feature = "server")]
thread_local! {pub static DB: rusqlite::Connection = {println!("DB init");let conn = rusqlite::Connection::open("note.db").unwrap();conn.execute_batch("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY,content TEXT NOT NULL)",).unwrap();conn};
}
修改我们之前保存信息的服务端方法,把保存到文件改为存储到数据库表中。
#[server]
pub async fn save_note(content: String) -> Result<(), ServerFnError> {println!("received note {}", content);// 存储到当前目录文件中 note.txt// std::fs::write("note.txt", content).unwrap();#[cfg(feature = "server")]{use crate::db::DB;let inserted = DB.with(|f| f.execute("INSERT INTO notes (content) VALUES (?1)", [&content])).expect("failed to insert into notes");println!("inserted {} rows", inserted);}Ok(())
}
同样的,在操作数据库也应该保证是在服务端运行#[cfg(feature = "server")]
,启动我们的程序dx serve --platform web
,在交互接口调用时,同时完成了数据初始化,并保存了数据到note.db
中。
可以看到当前服务目录下自动生成了note.db
文件,并且可以查看到数据已经保存到数据库中。
引用
-
dioxus
-
dioxus-doc
相关文章:

rust 全栈应用框架dioxus server
接上一篇文章dioxus全栈应用框架的基本使用,支持web、desktop、mobile等平台。 可以先查看上一篇文章rust 全栈应用框架dioxus👈 既然是全栈框架,那肯定是得有后端服务的,之前创建的服务没有包含后端服务包,我们修改…...
CSS Layer 详解
CSS Layer 详解 前言 最近在整理CSS知识体系时,发现Layer这个特性特别有意思。它就像是给样式规则提供了一个专属的「VIP通道」,让我们能更优雅地解决样式冲突问题。今天我就用最通俗的语言,带大家全面了解这个CSS新特性。 什么是CSS Laye…...

西安交大多校联训NOIP1模拟赛题解
西安交大多校联训NOIP1模拟赛题解 T1 秘境形式化题意思路代码(丑陋) T2 礼物形式化题意思路代码(实现) T3 小盒子的数论形式化题意思路代码(分讨) T4 猫猫贴贴(CF997E)形式化题意思路代码(深奥&…...

数据结构(三)——栈和队列
一、栈和队列的定义和特点 栈:受约束的线性表,只允许栈顶元素入栈和出栈 对栈来说,表尾端称为栈顶,表头端称为栈底,不含元素的空表称为空栈 先进后出,后进先出 队列:受约束的线性表࿰…...

若依定制pdf生成实战
一、介绍 使用 Java Apache POI 将文字渲染到 Word 模板是一种常见的文档自动化技术,广泛应用于批量生成或定制 Word 文档的场景。使用aspose可以将word转成pdf从而达到定制化pdf的目的。 参考文档:java实现Word转Pdf(Windows、Linux通用&a…...
RCE联系
过滤 绕过空格 ● 进制绕过 题目练习 数字rce 使用$0执行bash,<<<将后面的字符串传递给左边的命令。 例如: <?php highlight_file(__FILE__); function waf($cmd) { $whiteList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \\, \, $, <]; $cmd_ch…...

c++STL-vector的模拟实现
cSTL-vector的模拟实现 vector的模拟实现基本信息构造函数析构函数返回容量(capacity)返回元素个数(size)扩容(reserve和resize)访问([])迭代器(**iterator**)…...
C++核心编程解析:模板、容器与异常处理全指南
文章目录 一、模板1.1 定义1.2 作用1.3 函数模版1.3.1 格式 1.4 类模版1.4.1 格式1.4.2 代码示例1.4.3 特性 二、容器2.1 概念2.2 容器特性2.3 分类2.4 向量vector2.4.1 特性2.4.2 初始化与操作2.4.3 插入删除 2.5 迭代器2.6 列表(list)2.6.1 遍历方式2.…...

在 Elasticsearch 中连接两个索引
作者:来自 Elastic Kofi Bartlett 解释如何使用 terms query 和 enrich processor 来连接 Elasticsearch 中的两个索引。 更多有关连接两个索引的查询,请参阅文章 “Elastic:开发者上手指南” 中的 “丰富数据及 lookup” 章节。 Elasticsea…...
Linux常用命令详解(下):打包压缩、文本编辑与查找命令
一、打包压缩命令 在Linux系统中,打包与压缩是文件管理的核心操作之一。不同的工具适用于不同场景,以下是最常用的命令详解: 1. tar命令 作用:对文件进行打包、解包、压缩、解压。 语法: tar [选项] [压缩包名] […...

使用 Watt toolkit 加速 git clone
一、前言 Watt toolkit 工具是我经常用于加速 GitHub 网页和 Steam 游戏商店访问的工具,最近想加速 git clone,发现可以使用 Watt toolkit 工具的代理实现。 二、查看端口 我这里以 Ubuntu 为例,首先是需要将加速模式设置为 System࿱…...

应急响应靶机——WhereIS?
用户名及密码:zgsf/zgsf 下载资源还有个解题.exe: 1、攻击者的两个ip地址 2、flag1和flag2 3、后门程序进程名称 4、攻击者的提权方式(输入程序名称即可) 之前的命令: 1、攻击者的两个ip地址 先获得root权限,查看一下历史命令记录&#x…...

Docke容器下JAVA系统时间与Linux服务器时间不一致问题解决办法
本篇文章主要讲解,通过docker部署jar包运行环境后出现java系统内时间与服务器、个人电脑真实时间不一致的问题原因及解决办法。 作者:任聪聪 日期:2025年5月12日 问题现象: 说明:与实际时间不符,同时与服务…...

【MCP】其他MCP服务((GitHub)
【MCP】其他MCP服务((GitHub) 1、其他MCP服务(GitHub) MCP广场:https://www.modelscope.cn/mcp 1、其他MCP服务(GitHub) 打开MCP广场 找到github服务 访问github生成令牌 先…...
SQL:MySQL函数:日期函数(Date Functions)
目录 时间是数据的一种类型 🧰 MySQL 常用时间函数大全 🟦 1. 获取当前时间/日期 🟦 2. 日期运算(加减) 🟦 3. 时间差计算 🟦 4. 格式化日期 🟦 5. 提取时间部分 Ƿ…...

内存 -- Linux内核内存分配机制
内存可以怎么用? kmalloc:内核最常用,用于频繁使用的小内存申请 alloc_pages:以页框为单位申请,物理内存连续 vmalloc:虚拟地址连续的内存块,物理地址不连线 dma_alloc_coherent:常…...

关于读写锁的一些理解
同一线程的两种情况: 读读: public static void main(String[] args) throws InterruptedException {ReentrantReadWriteLock lock new ReentrantReadWriteLock();Lock readLock lock.readLock();Lock writeLock lock.writeLock();readLock.lock();S…...

C++修炼:模板进阶
Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路! 我的博客:<但凡. 我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》 欢迎点赞,关注&am…...

android-ndk开发(10): use of undeclared identifier ‘pthread_getname_np‘
1. 报错描述 使用 pthread 获取线程名字, 用到 pthread_getname_np 函数。 交叉编译到 Android NDK 时链接报错 test_pthread.cpp:19:5: error: use of undeclared identifier pthread_getname_np19 | pthread_getname_np(thread_id, thread_name, sizeof(thr…...

UI自动化测试框架:PO 模式+数据驱动
🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 1. PO 设计模式简介 什么是 PO 模式? PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成…...
大小端的判断方法
大小端(Endianness) 是计算机存储多字节数据(如整数、浮点数)时的两种不同方式,决定了字节在内存中的排列顺序。 1. 大端(Big-Endian) 高位字节存储在低地址,低位字节存储在高地址。…...

Java笔记4
第一章 static关键字 2.1 概述 以前我们定义过如下类: public class Student {// 成员变量public String name;public char sex; // 男 女public int age;// 无参数构造方法public Student() {}// 有参数构造方法public Student(String a) {} }我们已经知道面向…...
DAY22kaggle泰坦尼克号
参考了机器学习实战进阶:泰坦尼克号乘客获救预测_天池notebook-阿里云天池 数据处理省略 直接上模型 5.12.1 一些数据的正则化 这里我们将Age和fare进行正则化: from sklearn import preprocessing scale_age_fare preprocessing.StandardScaler().…...

2025年渗透测试面试题总结-渗透测试红队面试八(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 渗透测试红队面试八 二百一十一、常见中间件解析漏洞利用方式 二百一十二、MySQL用户密码存储与加密 …...

MiniMind:3块钱成本 + 2小时!训练自己的0.02B的大模型。minimind源码解读、MOE架构
大家好,我是此林。 目录 1. 前言 2. minimind模型源码解读 1. MiniMind Config部分 1.1. 基础参数 1.2. MOE配置 2. MiniMind Model 部分 2.1. MiniMindForCausalLM: 用于语言建模任务 2.2. 主干模型 MiniMindModel 2.3. MiniMindBlock: 模型的基本构建块…...

如何进行前端性能测试?--性能标准
如何进行前端性能测试?–性能标准 前端性能测试指标: 首次加载阶段 场景:用户首次访问网页,在页面还未完全呈现各种内容和功能时的体验。重要指标及原因 首次内容绘制(FCP - First Contentful Paint)…...

通信网络编程——JAVA
1.计算机网络 IP 定义与作用 :IP 地址是在网络中用于标识设备的数字标签,它允许网络中的设备之间相互定位和通信。每一个设备在特定网络环境下都有一个唯一的 IP 地址,以此来确定其在网络中的位置。 分类 :常见的 IP 地址分为 I…...

Off-Policy策略演员评论家算法SAC详解:python从零实现
引言 软演员评论家(SAC)是一种最先进的Off-Policy策略演员评论家算法,专为连续动作空间设计。它在 DDPG、TD3 的基础上进行了显著改进,并引入了最大熵强化学习的原则。其目标是学习一种策略,不仅最大化预期累积奖励&a…...

热门CPS联盟小程序聚合平台与CPA推广系统开发搭建:助力流量变现与用户增长
一、行业趋势:CPS与CPA模式成流量变现核心 在移动互联网流量红利见顶的背景下,CPS(按销售付费)和CPA(按行为付费)模式因其精准的投放效果和可控的成本,成为企业拉新与用户增长的核心工具。 CPS…...
(自用)Java学习-5.9(Thymeleaf,自动装配,自定义启动器 )
一、Thymeleaf 模板技术 片段定义与复用 <!-- 声明片段 --> <div th:fragment"b1">...</div> <!-- 插入片段 --> <div th:insert"~{bottom :: b1}"></div> <!-- 追加内容 --> <div th:replace"~{botto…...