salvo搭建rust web项目

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: salvo搭建rust web项目

这两天在研究如何搭建一个rust web项目用来练习rust,目前正好搭了一个小demo,先记录下来。

感兴趣的小伙伴可以在github上下载,也欢迎小伙伴给一个小星星,谢谢!

1.初始化项目

首先我们使用cargo命令创建一个bin项目:

cargo new hello_salvo --bin
复制代码

项目初始化成功后,我们使用idea导入。

2.添加依赖

项目初始化化,我们需要在Cargo.toml当中添加相应的依赖。

  • salvo依赖
#web框架
salvo = "0.27.0"
tokio = { version = "1", features = ["macros"] }
复制代码
  • 因为salvo依赖与tokio,所以想要的tokio我们也是需要导入的。salvo相当于java中的spring mvc的角色,承担了解析请求数据和写入响应数据的职责。它的底层通信是基于tokio实现的。
  • 序列化工具
# 序列化工具
serde = "1.0.140"
复制代码
  • serde是一个序列化工具,比如model转json字符串,json字符串转model。salvo里面类似的这种模型转换需要serde来提供。
  • 日志依赖
#日志依赖
tracing = "0.1.35"
tracing-subscriber = "0.3.15"
复制代码
  • 这个就是日志相关的依赖了,因为我们不能在项目当中到处写println,所以在项目初始化的时候就应该配置好日志处理。这个依赖的使用,以后也会花一些篇幅来讲解,这里暂时不细说。
  • 数据库依赖
#数据库依赖
mysql = "20.0.0"
once_cell = "1.13.0"
复制代码
  • 比较重要的就是数据库的依赖了,当前这个是mysql数据库的依赖。once_cell是用来初始化数据库连接池用的,它的作用是可以让对象初始化一次,相当于java当中的static作用,不排除还会在其他方面使用到它。
  • 时间处理工具
#处理时间
chrono = "0.4.19"
复制代码
  • chrono是为了接收从数据库中查询出时间类型的字段而引入进来的。不排除还会在其他方面使用到它。

3. 初始化代码

我们首先把项目结构规划好,先把相应的文件夹创建好:

42fdb2820dd84a68a564cc0ad6f5ae53_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

8913de8dd5fe49d69a524d3d5f55e88c_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

初始化日志

mod.rs

use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
pub fn init(){
    // 只有注册 subscriber 后, 才能在控制台上看到日志输出
    tracing_subscriber::registry().with(fmt::layer()).init();
}
复制代码

在mod.rs中给一个init函数,我们在main函数去调用,这样就能把整个模块初始化了。按照这个逻辑,其他的模块也这样实现模块初始化。

43692955f3cb47d3b4f90e98b161aca7_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png


初始化数据库

mod.rs

mod mysql_conn_pool;
pub mod account_mapper;
pub mod po;
// const DB_URL: &str = "mysql://数据库用户名:数据库密码@数据库ip:数据库端口/数据库名称";
pub fn init() {
    // 初始化链接池
    mysql_conn_pool::init_mysql_pool(DB_URL);
}
复制代码

mysql_conn_pool.rs

use mysql::{Pool, PooledConn};
use once_cell::sync::OnceCell;
use tracing::{instrument, info};
// 创建一个全局的DB_POOL,可以一直使用,启动的时候初始化即可
static DB_POOL: OnceCell<Pool> = OnceCell::new();
// 初始化数据库链接池
#[instrument]
pub fn init_mysql_pool(db_url: &str) {
    info!("初始化数据库线程池--------开始-------");
    DB_POOL.set(mysql::Pool::new(&db_url).expect(&format!("Error connecting to {}", &db_url)))
        .unwrap_or_else(|_| { info!("try insert pool cell failure!") });
    info!("初始化数据库线程池--------结束-------");
}
// 从链接链接池里面获取链接
#[instrument]
pub fn get_connect() -> PooledConn {
    info!("从链接池获取数据库链接----------开始----------");
    let conn = DB_POOL.get().expect("Error get pool from OneCell<Pool>").get_conn().expect("Error get_connect from db pool");
    info!("从链接池获取数据库链接----------结束----------");
    conn
}
复制代码

account_mapper.rs属于操作具体的数据表,类似于mybatis里面的Mapper作用。

use mysql::prelude::{BinQuery, Queryable, WithParams};
use mysql::params;
use crate::dao::mysql_conn_pool::get_connect;
use crate::dao::po::account::Account;
use tracing::error;
use std::error::Error;
use tracing_subscriber::util::SubscriberInitExt;
use uuid::Uuid;
use crate::error::error::GlobalError;
pub struct AccountMapper;
impl AccountMapper {
    pub fn get_by_id(id: &str) -> Option<Account> {
        // 获取数据库链接
        let mut conn = get_connect();
        // 根据id查询账号信息
        let query_result = conn.exec_first("select id,account,password,enabled,create_time,modify_time from account where id=:id", params!("id"=>id))
            .map(|row| {
                row.map(|(id, account, password, enabled, create_time, modify_time)| Account { id, account, password, enabled, create_time, modify_time })
            });
        // 判断是否查询到数据
        match query_result {
            Ok(result) => {
                result
            }
            Err(_) => {
                None
            }
        }
    }
    pub fn insert(account: &str, password: &str) -> Result<u64, GlobalError> {
        // 获取数据库链接
        let mut conn = get_connect();
        // 执行插入语句,目前id写死,后续会修改
        let x = match "insert into account (id,account,password,enabled,create_time,modify_time) values (?,?,?,1,now(),now())"
            .with(("123456", account, password))
            .run(&mut conn) {
            // 返回受影响的数据行数
            Ok(res) => {
                Ok(res.affected_rows())
            }
            Err(e) => {
                // error!(e);
                Err(GlobalError::new("创建账号失败", e.to_string().as_str()))
            }
        };
        x
    }
}
复制代码

account.rs

use chrono::NaiveDateTime;
use serde::Serialize;
//这个结构体就和数据表一致。一共就六个字段。
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
pub struct Account {
    pub id: String,
    pub account: String,
    pub password: String,
    pub enabled: i32,
    pub create_time: NaiveDateTime,
    pub modify_time: NaiveDateTime,
}

初始化请求路由

7130e920a6314c0e92b22aa608a987c8_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

我是把所有的请求路由全部放在了controller下面,所有路由的初始化也放在了里面。

mod.rs

use salvo::Router;
use tracing::{instrument, info};
mod user_controller;
mod vo;
// 按模块来初始化,这样就不用把所有的路由全部集中写到mod.rs里面了。
#[instrument]
pub fn init() -> Router {
    info!("收集所有的请求路由配置---------------开始---------------");
    let router = Router::new()
        .push(user_controller::init());
    info!(router=?router);
    info!("收集所有的请求路由配置---------------结束---------------");
    router
}
复制代码

user_controller

use salvo::prelude::{Request, Response, Router, handler, Json, Extractible};
use salvo::http::header::{self, HeaderValue};
use tracing::instrument;
use crate::error::error::GlobalError;
use crate::service::account_service::AccountService;
use serde::{Serialize, Deserialize};
// 在mod.rs初始化方法中被调用
#[instrument]
pub fn init() -> Router {
    Router::new()
        .push(Router::with_path("/user_info/<id>").get(get_user_info))
        .push(Router::with_hoop(add_header).get(hello_world))
        .push(Router::with_path("/create_account").post(create_account))
}
#[handler]
async fn add_header(res: &mut Response) {
    res.headers_mut().insert(header::SERVER, HeaderValue::from_static("Salvo"));
}
#[handler]
async fn hello_world() -> &'static str {
    "Hello World!"
}
#[handler]
async fn get_user_info(req: &mut Request, res: &mut Response) {
    let option_id = req.param::<String>("id");
    match option_id {
        None => {
            panic!("id不合法");
        }
        Some(id) => {
            let account = AccountService::query_user_info_by_id(&id);
            res.render(Json(account));
        }
    }
}
#[handler]
async fn create_account(user_info: UserInfo, res: &mut Response) -> Result<String, GlobalError> {
    let account = &user_info.account;
    let password = &user_info.password;
    match AccountService::add_account(account, password) {
        Ok(x) => {
            println!("受影响的行数:{}",x);
            Ok(String::from("成功!"))
        },
        Err(e) => Err(e)
    }
}
#[derive(Debug, Serialize, Deserialize, Extractible)]
#[extract(
default_source(from = "body", format = "json")
)]
struct UserInfo {
    pub account: String,
    pub password: String,


自定义error


aa33bc81da4145edac350d91c26e1779_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

error.rs

use salvo::{Depot, Request, Response, Writer, async_trait};
use salvo::http::StatusCode;
use salvo::prelude::Json;
use serde::Serialize;
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
pub struct GlobalError {
    // 提示信息
    msg: String,
    // 错误信息
    error: String,
}
impl GlobalError {
    pub fn new(msg: &str, error: &str) -> GlobalError {
        GlobalError {
            msg: String::from(msg),
            error: String::from(error),
        }
    }
}
#[async_trait]
impl Writer for GlobalError {
    async fn write(self, req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
        res.render(Json(self));
    }
}


初始化service


5b10b88b9ff54b8d84f8d41441f11805_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

  • 其实目前service我还没有啥要初始化的,所以mod.rs里面基本没写啥,等以后有需要再补充。
    mod.rs
pub mod account_service;
pub fn init(){
}
复制代码
  • account_service.rs
use crate::dao::po::account::Account;
use crate::dao::account_mapper::AccountMapper;
use core::fmt::Error;
use crate::error::error::GlobalError;
pub struct AccountService;
impl AccountService {
    // 根据id查询账户信息
    pub fn query_user_info_by_id(id: &str) -> Account {
        // 校验参数
        if id.len() <= 0 {
            panic!("id不合法");
        }
        // 查询数据库
        let query_result = AccountMapper::get_by_id(id);
        // 验证查询结果
        match query_result {
            None => {
                // 这里需要调整为自定义error,避免程序挂掉
                panic!("没有查到任何数据");
            }
            Some(account) => {
                account
            }
        }
    }
    // 添加账号
    pub fn add_account(account: &str, password: &str) -> Result<u64, GlobalError> {
        // 前面还要补充一些参数校验的过程
        AccountMapper::insert(account, password)
    }
}
复制代码

4.启动服务器

我把启动服务器的代码全部放在了main.rs里面

mod dao;
mod controller;
mod logs;
mod service;
mod error;
use salvo::prelude::{Server, TcpListener};
use tracing::{span, info, Level};
// 启动服务器
fn start_server(ip: &str, port: usize) -> Server<TcpListener> {
    Server::new(TcpListener::bind(&format!("{}:{}", ip, port)))
}
#[tokio::main]
async fn main() {
    // 初始化日志
    logs::init();
    let span = span!(Level::WARN, "main");
    let _enter = span.enter();
    info!("main function start");
    // 初始化数据库连接池
    dao::init();
    // 初始化service
    service::init();
    // 初始化请求路由
    let router = controller::init();
    // 启动服务
    start_server("127.0.0.1", 7878).serve(router).await;
}
复制代码

至此,整个项目就算是初步完成了,我们就可以按照这个项目结构来编写我们的业务代码了。

后续会逐步补充我在这个项目当中遇到的问题,也欢迎小伙伴一起学习rust。


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
5天前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
39 1
|
1月前
|
JavaScript Java 微服务
现代化 Java Web 在线商城项目技术方案与实战开发流程及核心功能实现详解
本项目基于Spring Boot 3与Vue 3构建现代化在线商城系统,采用微服务架构,整合Spring Cloud、Redis、MySQL等技术,涵盖用户认证、商品管理、购物车功能,并支持Docker容器化部署与Kubernetes编排。提供完整CI/CD流程,助力高效开发与扩展。
295 63
|
2月前
|
安全 JavaScript Java
java Web 项目完整案例实操指南包含从搭建到部署的详细步骤及热门长尾关键词解析的实操指南
本项目为一个完整的JavaWeb应用案例,采用Spring Boot 3、Vue 3、MySQL、Redis等最新技术栈,涵盖前后端分离架构设计、RESTful API开发、JWT安全认证、Docker容器化部署等内容,适合掌握企业级Web项目全流程开发与部署。
143 0
|
4月前
|
人工智能 安全 程序员
用 Colab 和 ngrok 免费部署你的 Web UI 项目,随时随地访问!
用 Colab 和 ngrok 免费部署你的 Web UI 项目,随时随地访问!
|
10月前
|
缓存 JSON 监控
如何在项目中保证 Web 组件化的性能
保证 Web 组件化的性能需要从多个方面入手,综合运用各种优化方法和策略。通过持续的优化和改进,能够提高组件化的整体性能,为用户提供更好的体验,同时也有助于提高项目的开发效率和质量。
230 64
|
10月前
|
存储 前端开发 JavaScript
如何在项目中高效地进行 Web 组件化开发
高效地进行 Web 组件化开发需要从多个方面入手,通过明确目标、合理规划、规范开发、加强测试等一系列措施,实现组件的高效管理和利用,从而提高项目的整体开发效率和质量,为用户提供更好的体验。
304 63
|
7月前
|
网络协议 Java Shell
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
383 7
|
10月前
|
监控 安全 测试技术
如何在实际项目中应用Python Web开发的安全测试知识?
如何在实际项目中应用Python Web开发的安全测试知识?
178 61
|
7月前
|
安全 Linux 开发工具
零基础构建开源项目OpenIM桌面应用和pc web- Electron篇
OpenIM 为开发者提供开源即时通讯 SDK,作为 Twilio、Sendbird 等云服务的替代方案。借助 OpenIM,开发者可以构建安全可靠的即时通讯应用,如 WeChat、Zoom、Slack 等。 本仓库基于开源版 OpenIM SDK 开发,提供了一款基于 Electron 的即时通讯应用。您可以使用此应用程序作为 OpenIM SDK 的参考实现。本项目同时引用了 @openim/electron-client-sdk 和 @openim/wasm-client-sdk,分别为 Electron 版本和 Web 版本的 SDK,可以同时构建 PC Web 程序和桌面应用(Wi
465 2
|
10月前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
1002 1

热门文章

最新文章