salvo搭建rust web项目

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 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。


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
25天前
|
机器学习/深度学习 人工智能 前端开发
机器学习PAI常见问题之web ui 项目启动后页面打不开如何解决
PAI(平台为智能,Platform for Artificial Intelligence)是阿里云提供的一个全面的人工智能开发平台,旨在为开发者提供机器学习、深度学习等人工智能技术的模型训练、优化和部署服务。以下是PAI平台使用中的一些常见问题及其答案汇总,帮助用户解决在使用过程中遇到的问题。
|
2月前
|
存储 开发框架 NoSQL
ASP.NET WEB——项目中Cookie与Session的用法
ASP.NET WEB——项目中Cookie与Session的用法
29 0
|
2月前
|
IDE API 开发工具
 鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Web组件
 鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Web组件
46 2
|
20天前
|
前端开发 JavaScript 数据管理
描述一个使用Python开发Web应用程序的实际项目经验,包括所使用的框架和技术栈。
使用Flask开发Web应用,结合SQLite、Flask-SQLAlchemy进行数据管理,HTML/CSS/JS(Bootstrap和jQuery)构建前端。通过Flask路由处理用户请求,模块化代码提高可维护性。unittest进行测试,开发阶段用内置服务器,生产环境可选WSGI服务器或容器化部署。实现了用户注册登录和数据管理功能,展示Python Web开发的灵活性和效率。
14 4
|
1月前
|
运维 JavaScript 前端开发
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
|
1月前
|
缓存 移动开发 监控
Star 1.3K!推荐一款可以远程调试任意Web项目的开源工具!
Star 1.3K!推荐一款可以远程调试任意Web项目的开源工具!
|
1月前
|
Java Maven Android开发
JAVA Web项目开发创建Web项目(第一天)
JAVA Web项目开发创建Web项目(第一天)
|
1月前
|
Rust 安全 程序员
拜登:“一切非 Rust 项目均为非法”,开发界要大变天?
白宫国家网络总监办公室(ONCD,以下简称网总办)在本周一发布的报告中说道:“程序员编写代码并非没有后果,他们的⼯作⽅式于国家利益而言至关重要。”
32 1
|
2月前
|
JSON IDE Java
创建一个简单的Spring Boot Web项目
创建一个简单的Spring Boot Web项目
51 1
|
2月前
|
JSON Rust 前端开发
Rust Web框架概览:Actix-Web与Yew的探索之旅
本文深入探讨了Rust编程语言中两个备受瞩目的Web框架:Actix-Web和Yew。我们将详细介绍这两个框架的核心特性、应用场景、性能优势以及如何使用它们构建高效、安全的Web应用。通过本文,您将更全面地了解Rust在Web开发领域的潜力和实践。