salvo搭建rust web项目

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 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。


相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
29天前
|
缓存 JSON 监控
如何在项目中保证 Web 组件化的性能
保证 Web 组件化的性能需要从多个方面入手,综合运用各种优化方法和策略。通过持续的优化和改进,能够提高组件化的整体性能,为用户提供更好的体验,同时也有助于提高项目的开发效率和质量。
42 8
|
29天前
|
存储 前端开发 JavaScript
如何在项目中高效地进行 Web 组件化开发
高效地进行 Web 组件化开发需要从多个方面入手,通过明确目标、合理规划、规范开发、加强测试等一系列措施,实现组件的高效管理和利用,从而提高项目的整体开发效率和质量,为用户提供更好的体验。
32 7
|
1月前
|
监控 安全 测试技术
如何在实际项目中应用Python Web开发的安全测试知识?
如何在实际项目中应用Python Web开发的安全测试知识?
32 4
|
1月前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
95 1
|
1月前
|
JavaScript 前端开发 开发工具
web项目规范配置(husky、eslint、lint-staged、commit)
通过上述配置,可以确保在Web项目开发过程中自动进行代码质量检查和规范化提交。Husky、ESLint、lint-staged和Commitlint共同作用,使得每次提交代码之前都会自动检查代码风格和语法问题,防止不符合规范的代码进入代码库。这不仅提高了代码质量,还保证了团队协作中的一致性。希望这些配置指南能帮助你建立高效的开发流程。
48 5
|
1月前
|
JavaScript 前端开发 数据安全/隐私保护
Web开发者必看:手把手教你如何轻松播放m3u8流地址,解锁视频播放新技能,让你的项目更上一层楼!
【10月更文挑战第23天】随着互联网技术的发展,m3u8格式因良好的兼容性和高压缩率被广泛用于网络流媒体传输。本文介绍如何在Web端播放m3u8流地址,包括引入视频播放器(如Video.js)、创建播放器容器、初始化播放器及播放m3u8流的具体步骤。此外,还涉及处理加密m3u8流的示例。
311 1
|
2月前
|
JSON 搜索推荐 API
Python的web框架有哪些?小项目比较推荐哪个?
【10月更文挑战第15天】Python的web框架有哪些?小项目比较推荐哪个?
80 1
|
2月前
|
前端开发 JavaScript API
惊呆了!学会AJAX与Fetch API,你的Python Web项目瞬间高大上!
在Web开发领域,AJAX与Fetch API是提升交互体验的关键技术。AJAX(Asynchronous JavaScript and XML)作为异步通信的先驱,通过XMLHttpRequest对象实现了局部页面更新,提升了应用流畅度。Fetch API则以更现代、简洁的方式处理HTTP请求,基于Promises提供了丰富的功能。当与Python Web框架(如Django、Flask)结合时,这两者能显著增强应用的响应速度和用户体验,使项目更加高效、高大上。
56 2
|
3月前
|
JSON Rust 安全
30天拿下Rust之实战Web Server
30天拿下Rust之实战Web Server
83 7
|
3月前
|
前端开发 Python
前后端分离的进化:Python Web项目中的WebSocket实时通信解决方案
在现代Web开发领域,前后端分离已成为一种主流架构模式,它促进了开发效率、提升了应用的可维护性和可扩展性。随着实时数据交互需求的日益增长,WebSocket作为一种在单个长连接上进行全双工通讯的协议,成为了实现前后端实时通信的理想选择。在Python Web项目中,结合Flask框架与Flask-SocketIO库,我们可以轻松实现WebSocket的实时通信功能。
73 2