rust 全栈应用框架dioxus server

接上一篇文章dioxus全栈应用框架的基本使用,支持web、desktop、mobile等平台。

可以先查看上一篇文章rust 全栈应用框架dioxus👈

既然是全栈框架,那肯定是得有后端服务的,之前创建的服务没有包含后端服务包,我们修改Cargo.toml,增加后端服务包,

指定依赖dioxus包含特性fullstack

toml 复制代码
dioxus = { version = "0.6.0", features = ['fullstack'] }

并且需要去掉当前默认的default平台设置,增加server服务端功能。之后需要重启项目,并且需要指定平台dx serve --platform web

toml 复制代码
[features]
default = ["web"] // 移除
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
server = ["dioxus/server"] // 增加

重新启动服务后,类似本地mock服务,可以像调用接口一样在前端组件逻辑中调用。

启动之后可以看到和仅前端服务不同的是多了一个fullstack.这样我们可以开始编写服务端代码了。

以一个简单的记录信息的服务为例,保存用户输入的信息,并展示用户已经保存的信息数据。

内连服务RPC

内连服务功能函数定义和界面定义代码没有分离。通过#[server]定义服务端功能函数,函数是异步async的。

我们定义保存用户输入信息的函数,返回值为Result<(), ServerFnError>,请求参数会自动被序列化,响应参数也必须可被序列化。

rust 复制代码
#[server]
async fn save_note(content:String) -> Result<(), ServerFnError> {
    Ok(())
}

在页面上通过点击事件,调用后端服务,dioxus使用axum来处理后端服务,前端则可以通过reqwest库请求服务。内连服务则可以让我们直接在事件处理方法中调用服务端函数。

axusm 是一个web服务框架,集成了tokio异步运行时;tower构建客户端和服务端;hyperhttp服务库。

定义好了服务端功能函数,在客户端通过点击事件进行调用。dioxus会自动处理调用到的服务函数,注册服务,建立调用关系。

rust 复制代码
#[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

rust 复制代码
#[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来标记依赖,这是因为某些依赖只在特定的平台中运行,然后在特定的平台特性中指定需要的依赖。

toml 复制代码
[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分离的目的。

rust 复制代码
#[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)

rust 复制代码
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目录中

rust 复制代码
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中,然后在不同的平台的引入并使用。

rust 复制代码
use dioxus::prelude::*;

use app::App as BaseApp;

#[component]
pub fn App() -> Element {
    rsx! {
        h2 { "Hello, web!" }
        BaseApp {}
    }
}

明确不同平台、不同能力划分的子包,可以更方便的管理,也能更好的针对不同平台进行定制化处理。

官方并没有给出一个标准示例,这方面还需要继续探索。#[server]可以将前后端写在一起,再区分模块是否不妥,还需实践。

路由

路由在业务开发中必不可少,dioxus 提供了特性router支持路由配置,我们修改依赖增加特性支持

toml 复制代码
[workspace.dependencies]
dioxus = { version = "0.6.3", features = ["fullstack", "router"] }

diosux提供了派生宏Routable使得我们通过枚举定义路由:

rust 复制代码
#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[route("/")]
    Home,
}

定义了默认导航路由地址/渲染组件Home,在需要渲染路由的地方使用Router::<Route> {}来占位路由渲染。修改组件App增加路由渲染占位:

rust 复制代码
#[component]
pub fn App() -> Element {
    rsx! {
        Router::<Route> {}
    }
}

我们定义Home组件,默认可以展示来自不同平台的平台名称,在各个平台中通过use_context_providerhook提供了平台名称。比如在web平台中:

rust 复制代码
use dioxus::prelude::*;

use app::App as BaseApp;

#[component]
pub fn App() -> Element {
    use_context_provider(|| "dioxus-web".to_string());
    rsx! {
        BaseApp {}
    }
}

Home组件中获取并展示,上下文变量共享可以作为不同平台的环境变量来处理一些特定的逻辑。

rust 复制代码
#[component]
pub fn Home() -> Element {
    let platform_name: String = use_context();
    rsx! {
        h2{
            "{platform_name}"
        }
    }
}

我们将原来的输入信息保存的功能提取成一个组件Note,并默认初始渲染这个组件,我们还希望Home组件也能展示,也就是Home组件是父组件,Note组件是子组件。

通过Outlet::<Route> {}将路由匹配的组件渲染到指定的位置,修改组件Home

rust 复制代码
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渲染匹配到的子组件。

rust 复制代码
#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[layout(Home)]
    #[route("/")]
    AddNote,
}

上面默认路由/渲染了Note,在日常开发中/路由渲染可能会发生改变,为了方便灵活配置,指定跳转到其他路由。新增一个路由/note,用来访问Note组件,然后路由/重定向到/note

rust 复制代码
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
enum Route {
    #[redirect("/",|| Route::AddNote)]
    #[layout(Home)]
        #[route("/note")]
        AddNote,
    #[end_layout]
}

为了标识嵌套关系,使用#[rustfmt::skip]保持手动缩进格式,使用#[redirect]重定向路由,第一个参数指定路径;第二个闭包函数返回渲染的路由(已经定义了的路由)。

当前重定向目标路由时,浏览器的访问路径并不会由/更改为/note

动态路由

动态路由包括动态路径和查询参数。

动态路径就是通过:name表示参数name可以是任意值。

rust 复制代码
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
enum Route {
    #[route("/note/:id")]
    ViewNote {id:String},
}

查询参数则是在路径后面加上?,后面跟:name,多个参数用&连接。

rust 复制代码
#[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]标识上级路径

rust 复制代码
#[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匹配所有路径段:

rust 复制代码
#[derive(Routable, Clone, PartialEq)]
enum Route {
    // ... routes

    #[route("/:..segments")]
    NotFound { segments: Vec<String> },
}

也可通过redirect重定向到首页去,我们在初始默认/渲染的Home组件,可以在路由重定向,当匹配不到其他路由路径时,渲染Home组件。

rust 复制代码
#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[route("/")]
    #[redirect("/:..segments",|segments:Vec<String>| Route::Home {})]
    Home,
}

对于处理路由匹配不到的处理只能选择其中一个配置,不能同时设置。

对于:..routes捕获剩余路由路径也可用于路由的嵌套路由中,比如#[route("/note/:..routes")]

路由导航

dioxus 提供了组件Link 用来跳转到指定路由。

rust 复制代码
#[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 返回下一级路由。
rust 复制代码
#[component]
pub fn Home() -> Element {
    let router = navigator();

    rsx! {
        button {
            onclick: move |_| {
                router.push(Route::AddNote {});
            },
            "Add Note"
        }
    }
}

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

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

rust 复制代码
#[component]
pub fn Add() -> Element {
    rsx! {
        GoBackButton {"back"}
    }
}

连接数据库

现在能用的数据库很多了,这里找一个简单的数据库测试存储。不需要额外安装的嵌入式数据库,比如SQLite

安装依赖rusqlite

sh 复制代码
cargo add rusqlite --optional

新增一个db.rs用于管理操作数据库,数据库操作只能在服务端运行, 我们需要使用#[cfg(feature = "server")]

rust 复制代码
#[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
    };
}

修改我们之前保存信息的服务端方法,把保存到文件改为存储到数据库表中。

rust 复制代码
#[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文件,并且可以查看到数据已经保存到数据库中。

引用

相关推荐
蜗牛沐雨5 小时前
Rust 中的 `PartialEq` 和 `Eq`:深入解析与应用
开发语言·后端·rust
Python私教5 小时前
Rust快速入门:从零到实战指南
开发语言·后端·rust
明月看潮生7 小时前
青少年编程与数学 02-019 Rust 编程基础 10课题、函数、闭包和迭代器
开发语言·青少年编程·rust·编程与数学
明月看潮生7 小时前
青少年编程与数学 02-019 Rust 编程基础 09课题、流程控制
开发语言·算法·青少年编程·rust·编程与数学
一丝晨光13 小时前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
景天科技苑13 小时前
【Rust泛型】Rust泛型使用详解与应用场景
开发语言·后端·rust·泛型·rust泛型
zhuziheniaoer14 小时前
rust-candle学习笔记11-实现一个简单的自注意力
笔记·学习·自然语言处理·rust
明月看潮生1 天前
青少年编程与数学 02-019 Rust 编程基础 08课题、字面量、运算符和表达式
开发语言·青少年编程·rust·编程与数学
天天打码1 天前
Rspack:字节跳动自研 Web 构建工具-基于 Rust打造高性能前端工具链
开发语言·前端·javascript·rust·开源