Я новичок в Rust, и я искал веб-фреймворк для создания сервера или API, и я нашел Axum на Github, и я хочу начать его использовать. Я хотел узнать об Axum, поэтому я начал писать эту статью, изучая его, чтобы закрепить некоторые его концепции и возможности.
DISCLAIMER: Это не исчерпывающее руководство по Axum, если вы хотите знать каждую функцию и как их использовать, то вот документация.
В этой статье мы просто будем использовать методы get()
и post()
и обслуживать HTML-файл.
Согласно документации:
axum — это фреймворк для веб-приложений, который фокусируется на эргономичности и модульности.Высокоуровневые возможности
- Маршрутизация запросов к обработчикам с помощью API без макросов.
- Декларативный разбор запросов с помощью экстракторов.
- Простая и предсказуемая модель обработки ошибок.
- Генерирование ответов с минимальным количеством шаблонов.
- Использование всех преимуществ экосистемы промежуточного ПО, сервисов и утилит tower и tower-http.
В частности, последний пункт отличает axum от существующих фреймворков. axum не имеет собственной системы промежуточного ПО, а использует tower::Service. Это означает, что axum получает таймауты, трассировку, сжатие, авторизацию и многое другое бесплатно. Это также позволяет вам делиться промежуточным ПО с приложениями, написанными с использованием hyper или tonic.
Давайте начнем импортировать наши зависимости.
Cargo.toml
[dependencies]
axum = "0.5.11"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1.35"
tracing-subscriber = "0.3.14"
serde = { version = "1.0.138", features = ["derive"] }
serde_json = "1.0"
main.rs
use axum::{
routing::{get, post},
http::StatusCode,
response::IntoResponse,
Json, Router};
use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> &'static str {
"Hello, World!"
}
Теперь давайте поговорим о приведенном выше коде. Сначала мы импортируем то, что собираемся использовать. И пишем #[tokio::main]
над нашей главной функцией, чтобы нам разрешили использовать async. Затем мы создаем экземпляр Router и вызываем метод route
с указанием пути и сервиса, который будет вызван, если путь совпадет, в данном случае root
, и он обернут в метод get
.
Затем мы используем SocketAddr
для определения порта, который мы будем использовать, и передаем ему IP-адрес localhost и номер порта, в данном случае ‘8000’.
Мы используем Server
и передаем ссылку addr
функции bind
.
Согласно документации:
Server — это основной способ начать прослушивать HTTP-запросы. Он оборачивает слушателя с помощью MakeService, а затем должен быть выполнен, чтобы начать обслуживать запросы.
Мы передаем app
в качестве аргумента методу serve
, но нам нужно использовать метод into_make_service
, потому что serve
получает make_service
в качестве параметра, а app
является экземпляром маршрутизатора.
Вот что говорится в документации о into_make_service
:
pub fn into_make_service(self) -> IntoMakeService< Self >
Преобразует этот маршрутизатор в MakeService, который представляет собой сервис, чьим ответом является другой сервис.
Это полезно при запуске вашего приложения с сервером hyper’s Server.
И MakeService
:
Создает новые значения Сервиса.
Действует как фабрика сервисов. Это полезно для случаев, когда необходимо создавать новые значения службы. В качестве примера можно привести слушателя TCP-сервера. Слушатель принимает новые TCP-потоки, получает новое значение Службы с помощью трейта MakeService и использует это новое значение Службы для обработки входящих запросов на этом новом TCP-потоке.
По сути, это псевдоним трейта для службы служб.
Вот ссылка, если вы хотите узнать больше о трейте MakeService.
Теперь запустим код
cargo run
В консоли должно появиться сообщение «listening on 127.0.0.1:3000», а если мы скопируем номер и вставим его в браузер, то увидим эту страницу:
Извлечение параметра из пути
Axum имеет множество экстракторов, см. документацию здесь
В этом примере мы будем использовать Path
для извлечения имени из пути и использования его для отправки JSON-сообщения.
...
use axum::extract::Path;
...
async fn json_hello(Path(name): Path<String>) -> impl IntoResponse {
let greeting = name.as_str();
let hello = String::from("Hello ");
(StatusCode::OK, Json(json!({"message": hello + greeting })))
}
В блоке кода выше мы используем impl IntoResponse
в качестве возвращаемого значения функции json_hello()
. Согласно документации:
Все, что реализует IntoResponse, может быть возвращено из обработчиков. Обычно вам не нужно реализовывать IntoResponse вручную, так как axum предоставляет реализации для многих распространенных типов.
Если вы хотите реализовать собственный тип ошибки, вот документация.
Теперь давайте обновим нашу функцию main()
.
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root))
.route("/hello/:name", get(json_hello));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Если мы напишем в браузере localhost:3000/hello/Carlos
, он покажет следующее:
Теперь давайте воспользуемся методом post()
, чтобы создать пользователя.
...
#[derive(Deserialize)]
struct CreateUser {
username: String,
}
#[derive(Debug, Serialize,Deserialize, Clone, Eq, Hash, PartialEq)]
struct User {
id: u64,
username: String,
}
...
async fn create_user(Json(payload): Json<CreateUser>,) -> impl IntoResponse {
let user = User {
id: 1337,
username: payload.username
};
(StatusCode::CREATED, Json(user))
}
В приведенном выше коде мы создаем две структуры: CreateUser и User. Мы используем JSON extractor, чтобы извлечь полезную нагрузку, то есть данные из JSON, передать их в качестве значения в поле username
, и вернуть переменную user
в виде JSON.
Добавляем маршрут post()
в функцию main()
.
...
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root))
.route("/hello/:name", get(json_hello))
.route("/user", post(create_user);
...
Обслуживание файлов
Для обслуживания файлов мы должны добавить tower-http
к зависимостям нашего проекта.
Cargo.toml
...
[dependencies]
...
tower-http = { version = "0.3.0", features = ["fs", "trace"] }
Мы создаем каталог и создаем в нем HTML-файл.
src
static/
hello.html
Cargo.toml
hello.html
<h1>Hello everyone</h1>
<h2>This is a static file</h2>
main.rs
Теперь мы обновим нашу функцию main()
, чтобы добавить маршрут, обслуживающий наш HTML-файл.
...
#[tokio::main]
async fn main() {
...
.route("/hello/:name", get(json_hello))
.route("/static", get_service(ServeFile::new("static/hello.html"))
.handle_error(|error: io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}));
...
}
Запустите код и перейдите на localhost:3000/static
, мы должны увидеть эту страницу:
Полный код
main.rs
use axum::{
routing::{get, post, get_service},
http::StatusCode,
response::IntoResponse,
Json, Router};
use axum::extract::Path;
use tower_http::services::ServeFile;
use std::net::SocketAddr;
use serde::{Deserialize, Serialize};
use serde_json::{json};
use std::{io};
#[derive(Deserialize)]
struct CreateUser {
username: String,
}
#[derive(Debug, Serialize,Deserialize, Clone, Eq, Hash, PartialEq)]
struct User {
id: u64,
username: String,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", get(root))
.route("/user", post(create_user))
.route("/hello/:name", get(json_hello))
.route("/static", get_service(ServeFile::new("static/hello.html"))
.handle_error(|error: io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> &'static str {
"Hello, World!"
}
async fn create_user(Json(payload): Json<CreateUser>) -> impl IntoResponse {
let user = User {
id: 1337,
username: payload.username
};
(StatusCode::CREATED, Json(user))
}
async fn json_hello(Path(name): Path<String>) -> impl IntoResponse {
let greeting = name.as_str();
let hello = String::from("Hello ");
(StatusCode::OK, Json(json!({"message": hello + greeting })))
}
Заключение
Axum — это первый веб-фреймворк, который я попробовал на Rust, и он мне понравился. Документация хорошо написана и имеет много примеров на странице Github. Один из аспектов, который мне понравился в нем — это API без макросов, другой аспект — это возможность цепочки маршрутов, так что я могу обернуть все свои обработчики в один блок кода.
Единственное, что мешает мне часто использовать его и делиться другими аспектами — это недостаток знаний в Rust, но я должен быть более дисциплинированным, и я буду, потому что хочу внести свой вклад в этот проект.
У Axum есть канал discord, сообщество действительно замечательное и полезное, вот ссылка.
Спасибо, что уделили время и прочитали эту статью.
Если у вас есть какие-либо рекомендации, советы, подсказки о том, как улучшить мой код, мой английский или что-либо еще, пожалуйста, оставьте комментарий или свяжитесь со мной через Twitter или LinkedIn.
Исходный код находится здесь.
Ссылка
Примеры
Документация Axum
Документация Tower_http
Документация Hyper