实战
发布 crate
步骤:
- 登录 https://crates.io 获取 API Token
- 使用
cargo login <token>
- 创建
cargo new --lib <name>
- 填写必须的清单
- 发布
cargo publish --registry crates-io
[package]
name = ""
description = ""
version = "0.1.0"
edition = "2021"
authors = [""]
license = ""
readme = "README.md"
Axum
- axum: web framework that focuses on ergonomics and modularity.
- tower: library for building robust clients and servers.
- hyper: fast and safe HTTP library for the Rust language.
- tokio: platform for writing asynchronous I/O backed applications.
- Serde: serialization/deserialization framework.
[dependencies]
# Web framework that focuses on ergonomics and modularity.
axum = "~0.7.0"
# Modular reusable components for building robust clients and servers.
tower = "~0.4.13"
# A fast and correct HTTP library.
hyper = { version = "~1.0.1", features = ["full"] }
# Event-driven, non-blocking I/O platform.
tokio = { version = "~1.34.0", features = ["full"] }
# A serialization/deserialization framework.
serde = { version = "~1.0.193", features = ["derive"] }
# Serde serializion/deserialization of JSON data.
serde_json = { version = "~1.0.108" }
HelloWorld
#[tokio::main]
async fn main() {
// build our application with a single route
let router = Router::new().route("/", get(|| async { "hello world" }));
// run our app with hyper, listening globally on port 3000
match tokio::net::TcpListener::bind("0.0.0.0:3000").await {
Ok(listener) => {
println!("Server run at http://localhost:3000");
axum::serve(listener, router).await.unwrap();
}
Err(err) => panic!("{err}"),
}
}
handle function
async fn hello() -> String {
"hello world".into()
}
let router = Router::new().route("/", get(hello));
router fallback
async fn fallback(uri: axum::http::Uri) -> impl axum::response::IntoResponse {
(
axum::http::StatusCode::NOT_FOUND,
format!("No route {}", uri),
)
}
let router = Router::new().fallback(fallback).route("/", get(hello));
response HTML
response text
use axum::{response::Html};
async fn get_html() -> Html<String> {
Html("<h1>hello world</h1>".to_string())
}
response file
async fn get_html() -> Html<String> {
Html(include_str!("./index.html").to_string())
}
response StatusCode
use axum::{http::StatusCode};
async fn status_code() -> http::StatusCode {
StatusCode::NOT_FOUND
}
response StatusCode and content
async fn status_code() -> (http::StatusCode, String) {
(StatusCode::NOT_FOUND, "不见了".to_string())
}
response Uri
use axum::{http::{Uri}};
async fn uri(uri: Uri) -> String {
format!("The URI is: {:?}", uri)
}
response Image
[dependencies]
base64 = "0.21.5"
http = "1.0.0"
use axum::{
response::{AppendHeaders, IntoResponse},
};
use http::header;
use base64::{engine, Engine};
async fn get_image() -> impl IntoResponse {
let png = concat!(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB",
"CAYAAAAfFcSJAAAADUlEQVR42mPk+89Q",
"DwADvgGOSHzRgAAAAABJRU5ErkJggg=="
);
(
AppendHeaders([(header::CONTENT_TYPE, "image/png")]),
engine::general_purpose::STANDARD.decode(png).unwrap(),
)
}
multiple HTTP verbs
axum routes can use HTTP verbs, including GET, PUT, PATCH, POST, DELETE.
/// axum handler for "GET /foo" which returns a string message.
/// This shows our naming convention for HTTP GET handlers.
async fn get_foo() -> String {
"GET foo".to_string()
}
/// axum handler for "PUT /foo" which returns a string message.
/// This shows our naming convention for HTTP PUT handlers.
async fn put_foo() -> String {
"PUT foo".to_string()
}
/// axum handler for "PATCH /foo" which returns a string message.
/// This shows our naming convention for HTTP PATCH handlers.
async fn patch_foo() -> String {
"PATCH foo".to_string()
}
/// axum handler for "POST /foo" which returns a string message.
/// This shows our naming convention for HTTP POST handlers.
async fn post_foo() -> String {
"POST foo".to_string()
}
/// axum handler for "DELETE /foo" which returns a string message.
/// This shows our naming convention for HTTP DELETE handlers.
async fn delete_foo() -> String {
"DELETE foo".to_string()
}
let app = axum::Router::new()
…
.route("/foo",
get(get_foo)
.put(put_foo)
.patch(patch_foo)
.post(post_foo)
.delete(delete_foo),
)
Extractors
An axum "extractor" is how you pick apart the incoming request in order to get any parts that your handler needs.
This section shows how to:
- Extract path parameters
- Extract query parameters
- Extract a JSON payload
- Respond with a JSON payload
use axum::{extract};
use serde_json::{json, Value};
// Extract path parameters
async fn get_id_with_path(extract::Path(id): extract::Path<String>) -> String {
format!("get id: {id}")
}
// Extract query parameters
async fn get_id_with_query(
extract::Query(params): extract::Query<HashMap<String, String>>,
) -> String {
format!("get items with query params: {params:?}")
}
async fn get_json() -> extract::Json<Value> {
json!({
"code": 200,
"success": true,
"payload": {
"features": [
"serde",
"json"
],
"homepage": null
}})
.into()
}
async fn put_json(extract::Json(data): extract::Json<Value>) -> String {
format!("put JSON data: {data}")
}
CORS
# 启用 cors
tower-http = { version = "0.5.0", features = ["fs", "cors"] }
use http::Method;
use tower_http::{
cors::{Any, CorsLayer}
};
let cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.allow_origin(Any);
Router::new()
.route("/", get(|| async { "hello world" }))
.layer(cors);
构建多个二进制文件
src/main.rs
是一个入口点,Rust 允许多个入口点构建不同的二进制文件
[[bin]]
# src/bin/one.rs or src/bin/one/main.rs
name = "one"
[[bin]]
# src/bin/two.rs or src/bin/two/main.rs
name = "two"
当不遵循约定时需要指定路径
[[bin]]
name = "app"
path = "src/app.rs"
爬虫
- 请求库 - reqwest
- 解析库 - scraper
use scraper::{Html, Selector};
fn main() {
let html = r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello, World</title>
</head>
<body>
<h1 class="foor">Hello, <i>World</i></h1>
</body>
</html>
"#;
let document = Html::parse_document(html);
let selector = Selector::parse("title").unwrap();
for element in document.select(&selector) {
println!("{:?}", element.text().collect::<Vec<_>>());
}
}
Actix Web
[dependencies]
actix-web = "4.5.1"
actix-cors = "0.6.5"
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use actix_cors::Cors;
async fn index() -> impl Responder {
HttpResponse::Ok().body("Hey there!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let port = 3000;
println!("Server running at http://localhost:{port}");
HttpServer::new(|| {
// 配置 CORS
let cors = Cors::permissive();
App::new()
.wrap(cors)
.route("/", web::get().to(index))
})
.bind(("localhost", port))?
.run()
.await
}
App state
#[derive(Debug, Clone)]
struct AppState {
foo: string,
}
let foo = "foo".to_string();
let state = AppState { foo };
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(state.clone()))
})
.bind(server_url)?
.workers(workers)
.run()
.await;
macro
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
App::new().wrap(cors).service(index)
multiple HTTP verbs
async fn index() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
App::new().wrap(cors).service(
resource("/")
.route(web::get().to(index)) // GET /
.route(web::post().to(index)), // POST /
)
response HTML
async fn get_html() -> impl Responder {
HttpResponse::Ok()
.content_type(ContentType::html())
.body("<h1>Hello World</h1>")
}
response HTML file
async fn get_html() -> impl Responder {
HttpResponse::Ok()
.content_type(ContentType::html())
.body(include_str!("./index.html"))
}
scope
async fn foo() -> impl Responder {
HttpResponse::Ok().body("foo")
}
async fn bar() -> impl Responder {
HttpResponse::Ok().body("bar")
}
App::new().wrap(cors).service(
web::scope("/v1/api")
.route("/foo", web::get().to(foo))
.route("/bar", web::get().to(bar)),
)
response StatusCode
async fn get_status_code() -> impl Responder {
HttpResponse::NotFound()
}
response StatusCode and content
async fn get_status_code() -> impl Responder {
HttpResponse::NotFound().body("不见了")
}
response Image
async fn get_image() -> impl Responder {
let png = concat!(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB",
"CAYAAAAfFcSJAAAADUlEQVR42mPk+89Q",
"DwADvgGOSHzRgAAAAABJRU5ErkJggg=="
);
HttpResponse::Ok()
.content_type(ContentType::png())
.body(engine::general_purpose::STANDARD.decode(png).unwrap())
}
response JSON
async fn get_json() -> impl Responder {
HttpResponse::Ok().json(json!({"a":"b"}))
}
Extractors
use SeaORM
Anyhow
anyhow
用于简化错误处理和提供更好的错误报告。这个库适合用于应用程序,而不是用于创建库,因为它提供了一个非结构化的,方便使用的错误类型
Rust 的标准库提供了 Result 和 Option 类型用于错误处理,但它们通常需要指定错误类型。与此不同,anyhow::Result 允许更简单地创建和处理错误
任何返回 Result 的函数都可以轻松地改为返回 anyhow::Result
fn do_something() -> Result<(), std::io::Error> {
//...
Ok(())
}
// 使用 anyhow::Result
fn do_something_anyhow() -> anyhow::Result<()> {
//...
Ok(())
}
SeaORM
选择数据库和运行时
[dependencies]
sea-orm = { version = "0.12", features = [
"sqlx-mysql",
"runtime-tokio-rustls",
"macros",
] }
安装sea-orm-cli
用于迁移数据或生成实体
cargo install sea-orm-cli
迁移
将模型同步到数据库中
sea migration init Initialize migration directory
sea migration generate Generate a new, empty migration
sea migration fresh Drop all tables from the database, then reapply all migrations
sea migration refresh Rollback all applied migrations, then reapply all migrations
sea migration reset Rollback all applied migrations
sea migration status Check the status of all migrations
sea migration up Apply pending migrations
sea migration down Rollback applied migrations
sea migration help Print this message or the help of the given subcommand(s)
Sea ORM Migration 系统会记录已执行过的迁移文件,如果直接修改已执行过的迁移文件,这些更改不会自动应用。要更新架构,有如下选择:
- 生成新的迁移文件,
sea migrate generate <filename>
- 如果是开发环境,可直接
sea migarte fresh
重新执行所有的迁移
从现有数据库中生成实体
需要提供DATABASE_URL
,默认会读取.env
DATABASE_URL=mysql://username:password@host/database
使用下面生成实体,实体是进行 CRUD 关键
sea-orm-cli generate entity -o src/entitys
Select
定义实体后就可以进行查询了
根据主键查询
// 查询主键为 1 的一条记录
Entity::find_by_id(1).one(db).await;
// 查询复合主键为 1,2 的一条记录
Entity::find_by_id(1, 2).one(db).await;
查询所有数据
Entity::find().all(db).await;
条件查询
// WHERE id BETWEEN 2 AND 3
Entity::find().filter(Column::Id.between(2, 3)).all(db).await;
// WHERE id NOT BETWEEN 2 AND 3
Entity::find().filter(Column::Id.not_between(2, 3)).all(db).await;
// WHERE Name LIKE foo
Entity::find().filter(Column::Name.like("foo")).all(db).await;
// WHERE Name NOT LIKE foo
Entity::find().filter(Column::Name.not_like("foo")).all(db).await;
// name = foo%
Entity::find().filter(Column::Name.starts_with("foo")).all(db).await;
// name = %foo
Entity::find().filter(Column::Name.ends_with("foo")).all(db).await;
// name = %foo%
Entity::find().filter(Column::Name.contains("foo")).all(db).await;
Insert
插入一个 Model
let model = ActiveModel {
name: Set("foo".to_owned()),
..Default::default()
};
Entity::insert(model).exec(&db).await?;
插入多个 Model
let foo = ActiveModel {
name: Set("foo".to_owned()),
..Default::default()
};
let bar = ActiveModel {
name: Set("bar".to_owned()),
..Default::default()
};
Entity::insert_many([foo, bar]).exec(&db).await?;
Update
更新一个 Model
let model = ActiveModel {
id: Set(1),
name: Set("Orange".to_owned()),
..Default::default()
};
Entity::update(model.clone())
.filter(fruit::Column::Name.contains("foo"))
.exec(&db)
.await?,
更新多个 Model
Entity::update_many()
.col_expr(Column::Id, Expr::value(Value::Int(None)))
.filter(fruit::Column::Name.contains("foo"))
.exec(&db)
.await?;
Delete
删除一个 Model
let model = ActiveModel {
id: Set(3),
..Default::default()
};
// WHERE "id" = $1",
Entity::delete(model).exec(&db).await?;
根据主键删除 Model
// WHERE "id" = $1",
Entity::delete_by_id(1).exec(&db).await?;
删除多个 Model
Entity::delete_many()
.filter(Column::Name.contains("foo"))
.exec(&db)
.await?;
静态链接
Rust 编译器默认情况下会采用动态链接的方式来链接 C/C++ 标准库:
- Windows:Rust 默认使用 MSVCRT(Microsoft Visual C++ Runtime)作为 C 标准库
- Linux:Rust 默认使用 glibc(GNU C Library)作为 C 标准库
优点是:更小的可执行体积文件,易于更新,更好的系统集成,而缺点则是:依赖性更强,运行时性能略有损失
想避免这些缺点,可以选择静态链接
在 Windows 为.cargo/config.toml
添加以下内容开启静态链接
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
在 Linux 中则添加以下内容使用 MUSL 作为静态链接
[target.x86_64-unknown-linux-musl]
rustflags = [
"-C", "target-feature=+crt-static",
"-C", "link-self-contained=yes",
]
OpenDAL
Rouille
Rouille 是一个微型 web 同步I/O框架库,但是它的性能也是足够的
[dependencies]
rouille = "3.6.2"
use rouille::Request;
use rouille::Response;
rouille::start_server("0.0.0.0:80", move |request| {
Response::text("hello world")
});
router
pub fn handle_routing(request: &Request) -> Response {
router!(request,
(GET) (/) => {
Response::text("hello world")
},
(GET) (/get_html) => {
Response::html("html")
},
(GET)(/user) => {
if let Ok(user) = store.get_users() {
Response::json(&ResponseJson{ data: user, code: 1})
} else {
Response::json(&ResponseJson::<Vec<i32>>{ data: vec![], code: 0})
}
},
(POST) (/user) => {
let body = try_or_400!(post_input!(request, {
id: String,
display_name: String
}));
if let Ok(_) = store.create_user(body.id, body.display_name) {
Response::text("user created successfully!")
} else {
Response::text("user created failed!")
}
},
_ => log(request, io::stdout(), || Response::empty_404())
)
}
middleware
use std::io;
use rouille::{log, Response};
pub fn log_middleware(request: &rouille::Request, response: Response) -> Response {
log(request, io::stdout(), || response)
}
pub fn cors_middleware(request: &rouille::Request, response: Response) -> Response {
let response = response
.with_additional_header("Access-Control-Allow-Origin", "*")
.with_additional_header("Access-Control-Allow-Methods", "*")
.with_additional_header("Access-Control-Allow-Headers", "*");
if request.method() == "OPTIONS" {
Response::empty_204()
.with_additional_header("Access-Control-Allow-Origin", "*")
.with_additional_header("Access-Control-Allow-Methods", "*")
.with_additional_header("Access-Control-Allow-Headers", "*")
} else {
response
}
}
start_server("localhost:3000", move |request| {
log_middleware(request, cors_middleware(request, handle_routing(request)))
});
Prisma Client Rust
Prisma Client Rust 是一个自动生成的查询构建器,它利用 Prisma 生态系统提供简单且完全类型安全的数据库访问
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.11", default-features = false, features = [
"sqlite",
] }