Axum
axum
官网地址:https://docs.rs/axum/latest/axum/
从hello world认识axum
开始
#![allow(unused)]
use std::net::SocketAddr;
use axum::{response::Html, routing::get, Router};
#[tokio::main]
async fn main() {
//1.create router
let hello_routes = Router::new().route(
"/hello",
get(|| async { Html("<strong>Hello World<strong/>") }),
);
//2.create server
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("--> LISTEN ON {addr}\n");
//3.run server
axum::Server::bind(&addr)
.serve(hello_routes.into_make_service())
.await
.unwrap();
}
对于hello_routes.into_make_service()
中这个into_make_service函数,文档中是这样描述的:
Convert this router into a [
MakeService
], that is a [Service
](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) whose response is another service.This is useful when running your application with hyper’s [
Server
](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html):
理解上面一句话,我们需要理解下面几个概念:
- Router: 在http请求中,router负责基于http请求路径和请求方法将http请求映射到正确的http handler上面。
- Hyper: 一个rust中的http库。
- Service: 在web框架中,service是http handler的抽象。它负责实际上的接受请求并且进行相应。
- MakeService: 这是一个比service稍微高级一点的抽象,它实际上是创建Service实例的工厂。 每次新的连接被建立的时候,MakeService就会被调用创建一个处理着恶搞连接的请求处理器。
因此这个函数的作用是将Router转换成MakeService,是为了创建一个更有效和更有弹性的处理http connection的方式。
什么?不想每次写完api都要去打开浏览器或者http client进行测试? 这里推荐一个更方便的方式来便利我们的开发:
1、安装依赖:
cargo add --dev httpc_test cargo install cargo-watch
2、编写测试类:
#![allow(unused)] use anyhow::Result; #[tokio::test] async fn quick_dev() -> Result<()> { let hc = httpc_test::new_client("http://localhost:8000")?; hc.do_get("/hello").await?.print().await?; Ok(()) }
3、查看结果:
- 使用cargo-watch来监听文件的变化:
cargo watch -q -c -w src/ -x run
执行上面的命令,cargo会以watch的形式监听src下面文件的变化,实现热更新,无需重新编译项目。
- 使用cargo-watch来监听测试类:
cargo watch -q -c -w tests/ -x "test -q quick_dev" -- --nocapture
--nocapture
是指不对标准输出进行捕获,比如println!
。这样,每次当我们对文件进行修改并保存后,cargo watch都会帮我们进行热更新。
自定义handler
你有没有注意到,现在我们是以闭包作为我们请求的handler,这在处理简单的场景下是十分方便的,但是当情况复杂起来了,闭包函数就不太能适用了。
我们定义如下的hello_handler
:
async fn hello_handler() -> impl IntoResponse {
println!("--> REQUEST: /hello");
Html("<h1>Hello, World!</h1>")
}
这里的返回值一般是你或者项目的构建者创建的统一返回结果CommonResult
。
请求参数获取
我们需要安装serde
依赖来帮助我们处理请求中的参数:
cargo add serde --features derive
- 处理
/hello?name=blkcor
,
#[derive(Debug, Deserialize)]
struct HelloParams {
name: Option<String>,
}
async fn hello_handler(params: <HelloParams>) -> String {
println!("--> REQUEST: /hello --> {:?}", params);
let name = params.name.as_deref().unwrap_or("world");
format!("Hello, {}!", name)
}
- 处理
/hello/blkcor
//路由如下:route("/hello2/:name", get(hello_handler2));
async fn hello_handler2(Path(name): Path<String>) -> String {
println!("--> REQUEST: /hello2 -->");
format!("Hello, {}!", name)
}
自定义router
将我们的hello_router提取出来成一个函数:
fn routes_hello() -> Router {
Router::new()
.route("/hello", get(hello_handler))
.route("/hello2/:name", get(hello_handler2))
}
将所有的分支路由合并到根路由上:
...
let routes_all = Router::new().merge(routes_hello());
...
静态文件服务
我们需要安装tower-http
帮助我们实现静态服务器的功能:
cargo add tower-http --features fs
编写静态文件路由:
fn routes_static() -> Router {
Router::new().nest_service("/", get_service(ServeDir::new("./")))
}
注册路由:
let routes_all = Router::new()
...
.fallback_service(routes_static());
这里的fallback_service
是对前面路由全部匹配失败后的兜底操作,我们当然也可以定义我们自己的静态资源处理路径,比如static
。
编写API
一般在编写项目的业务代码之前,我们需要先编写通用的方法或者对象。
创建error模块编写通用错误处理
创建/error/main.rs
文件:
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
LoginFail,
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
println!("->> {:<12} - {self:?}", "INTO_RES");
(StatusCode::INTERNAL_SERVER_ERROR, "UNHANDED_CLIENT_ERROR").into_response()
}
}
在main.rs
中引入mod:
pub use self::error::{Error, Result};
mod error;
创建web模块编写api
新建/web/mod.rs
文件:这个文件是web
目录下其他mod的入口:
pub mod routes_login;
在routes_login
这个模块中实现具体的登录逻辑:
use axum::{routing::post, Json, Router};
use serde::Deserialize;
use serde_json::{json, Value};
pub use crate::error::{Error, Result};
pub fn routes_login() -> Router {
Router::new().route("/api/login", post(api_login))
}
#[derive(Debug, Deserialize)]
struct LoginPayload {
username: String,
pwd: String,
}
async fn api_login(payload: Json<LoginPayload>) -> Result<Json<Value>> {
println!("->> {:<12} - api_login", "HANDLER");
//TODO: Implement real db/auth logic.
if (payload.username != "blkcor" || payload.pwd != "123456") {
return Err(Error::LoginFail);
}
//Set cookie.
//Create a successful body
let body = Json(json!(
{
"result":{
"success":true
}
}
));
Ok(body)
}
在main.rs
中引入web
这个mod并且注册路由:
...
mod web;
#[tokio::main]
async fn main() {
//1.create router
let routes_all = Router::new().merge(web::routes_login::routes_login());
...
}
分别测试一下成功和失败的结果:
...
hc.do_post(
"/api/login",
json!({
"username":"blkcor",
"pwd":"123456"
}),
)
.await?
.print()
.await?;
...
![CleanShot 2023-12-06 at 13.31.31](/Users/chenzilong/Library/Application Support/CleanShot/media/media_7VMBtLTs4G/CleanShot 2023-12-06 at 13.31.31.png)
...
hc.do_post(
"/api/login",
json!({
"username":"blkcor",
"pwd":"12345678"
}),
)
.await?
.print()
.await?;
...
![CleanShot 2023-12-06 at 13.32.25](/Users/chenzilong/Library/Application Support/CleanShot/media/media_DmEiWfA7ZK/CleanShot 2023-12-06 at 13.32.25.png)
Ok,非常棒,错误码和反回信息都打印出来了!
创建响应对象映射
我们需要特殊的一层来拦截响应并做一些最后需要进行的工作:(比如添加cookie)。
官方的描述是这样的:Create a middleware from an async function that transforms a response.就是对我们的response进行一定的转换。
- 安装
tower-cookies
cargo add tower-cookies
- 补充login中的添加cookie逻辑
async fn api_login(cookies: Cookies, payload: Json<LoginPayload>) -> Result<Json<Value>> {
println!("->> {:<12} - api_login", "HANDLER");
//TODO: Implement real db/auth logic.
if (payload.username != "blkcor" || payload.pwd != "123456") {
return Err(Error::LoginFail);
}
//Set cookie.
cookies.add(Cookie::new(web::AUTH_TOKEN, "user-1.exp.sign"));
//Create a successful body
let body = Json(json!(
{
"result":{
"success":true
}
}
));
Ok(body)
}
- 注意,我们需要添加CookieManagerLayer才能在使用cookie
let routes_all = Router::new()
...
.layer(CookieManagerLayer::new());
接下来编写我们的layer并使用:
async fn main_response_mapper(res: Response) -> Response {
println!("->> {:<12} - main_response_mapper", "RES_MAPPER");
//set cookie
res
}
let routes_all = Router::new()
...
.layer(map_response(main_response_mapper))
...
里面的逻辑将在之后完成。
编写model
创建model.rs
: