rust 全栈应用框架dioxus server

news2025/5/13 9:16:28

接上一篇文章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构建客户端和服务端;hyperhttp服务库。

定义好了服务端功能函数,在客户端通过点击事件进行调用。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.txt
    std::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")]条件判断只有在featuresserver时才编译代码,这样在启动web平台时不会自动将服务端代码进行编译,达到web和server分离的目的。

#[cfg(feature = "server")]
async fn launch_server() {
    // 获取到服务ip 端口
    // 这里依赖了`dioxus`的features cli_config
    let 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和前端,然后前端又可以区分为webmobiledesktop,将公共的页面逻辑放在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_providerhook提供了平台名称。比如在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"
        }
    }
}

虽然功能上navigatorLink类似,但是对于外部链接navigator并不保证跳转成功。

为了方便路由的前进、后退,dioxus提供了全局组件GoBackButtonGoForwardButton直接使用,避免了通过点击事件处理函数手动跳转。

#[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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2374545.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

西安交大多校联训NOIP1模拟赛题解

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

数据结构(三)——栈和队列

一、栈和队列的定义和特点 栈&#xff1a;受约束的线性表&#xff0c;只允许栈顶元素入栈和出栈 对栈来说&#xff0c;表尾端称为栈顶&#xff0c;表头端称为栈底&#xff0c;不含元素的空表称为空栈 先进后出&#xff0c;后进先出 队列&#xff1a;受约束的线性表&#xff0…

若依定制pdf生成实战

一、介绍 使用 Java Apache POI 将文字渲染到 Word 模板是一种常见的文档自动化技术&#xff0c;广泛应用于批量生成或定制 Word 文档的场景。使用aspose可以将word转成pdf从而达到定制化pdf的目的。 参考文档&#xff1a;java实现Word转Pdf&#xff08;Windows、Linux通用&a…

c++STL-vector的模拟实现

cSTL-vector的模拟实现 vector的模拟实现基本信息构造函数析构函数返回容量&#xff08;capacity&#xff09;返回元素个数&#xff08;size&#xff09;扩容&#xff08;reserve和resize&#xff09;访问&#xff08;[]&#xff09;迭代器&#xff08;**iterator**&#xff09…

在 Elasticsearch 中连接两个索引

作者&#xff1a;来自 Elastic Kofi Bartlett 解释如何使用 terms query 和 enrich processor 来连接 Elasticsearch 中的两个索引。 更多有关连接两个索引的查询&#xff0c;请参阅文章 “Elastic&#xff1a;开发者上手指南” 中的 “丰富数据及 lookup” 章节。 Elasticsea…

使用 Watt toolkit 加速 git clone

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

应急响应靶机——WhereIS?

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

Docke容器下JAVA系统时间与Linux服务器时间不一致问题解决办法

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

【MCP】其他MCP服务((GitHub)

【MCP】其他MCP服务&#xff08;&#xff08;GitHub&#xff09; 1、其他MCP服务&#xff08;GitHub&#xff09; MCP广场&#xff1a;https://www.modelscope.cn/mcp 1、其他MCP服务&#xff08;GitHub&#xff09; 打开MCP广场 找到github服务 访问github生成令牌 先…

内存 -- Linux内核内存分配机制

内存可以怎么用&#xff1f; kmalloc&#xff1a;内核最常用&#xff0c;用于频繁使用的小内存申请 alloc_pages&#xff1a;以页框为单位申请&#xff0c;物理内存连续 vmalloc&#xff1a;虚拟地址连续的内存块&#xff0c;物理地址不连线 dma_alloc_coherent&#xff1a;常…

关于读写锁的一些理解

同一线程的两种情况&#xff1a; 读读&#xff1a; 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大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》 欢迎点赞&#xff0c;关注&am…

android-ndk开发(10): use of undeclared identifier ‘pthread_getname_np‘

1. 报错描述 使用 pthread 获取线程名字&#xff0c; 用到 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 模式+数据驱动

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1. PO 设计模式简介 什么是 PO 模式&#xff1f; PO&#xff08;PageObject&#xff09;设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成…

Java笔记4

第一章 static关键字 2.1 概述 以前我们定义过如下类&#xff1a; public class Student {// 成员变量public String name;public char sex; // 男 女public int age;// 无参数构造方法public Student() {}// 有参数构造方法public Student(String a) {} }我们已经知道面向…

2025年渗透测试面试题总结-渗透测试红队面试八(题目+回答)

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

MiniMind:3块钱成本 + 2小时!训练自己的0.02B的大模型。minimind源码解读、MOE架构

大家好&#xff0c;我是此林。 目录 1. 前言 2. minimind模型源码解读 1. MiniMind Config部分 1.1. 基础参数 1.2. MOE配置 2. MiniMind Model 部分 2.1. MiniMindForCausalLM: 用于语言建模任务 2.2. 主干模型 MiniMindModel 2.3. MiniMindBlock: 模型的基本构建块…

如何进行前端性能测试?--性能标准

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

通信网络编程——JAVA

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

Off-Policy策略演员评论家算法SAC详解:python从零实现

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