嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第二十天!
昨天我們完成了個人部落格後端的完整規劃,從個人創作者的角度分析了需求,設計了簡潔而實用的資料模型和 API 架構。今天我們要面臨一個重要的決定:為我們的個人部落格選擇最合適的 Web 框架?
在 Rust 的 Web 開發生態系中,有兩個框架特別受到關注:Actix Web 和 Axum。今天我們就從個人專案的角度來深入比較這兩個框架,並最終為我們的個人部落格做出選擇!
對於個人部落格專案,我們的關注點和大型企業專案會有所不同:
個人開發者的需求:
讓我們從這些角度來比較兩個框架!
Actix Web 是 Rust 生態系中最早成熟的 Web 框架之一,在個人專案中有什麼優勢?
1. 成熟穩定,問題少
use actix_web::{web, App, HttpResponse, HttpServer, Result};
// 簡單直接的 API 設計
async fn get_post(path: web::Path<i32>) -> Result<HttpResponse> {
let post_id = path.into_inner();
// 個人部落格通常文章不多,簡單查詢即可
let post = fetch_post_from_db(post_id).await;
Ok(HttpResponse::Ok().json(post))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/posts/{id}", web::get().to(get_post))
})
.bind("127.0.0.1:3000")?
.run()
.await
}
2. 豐富的生態和範例
對於個人開發者來說,能找到現成的解決方案非常重要:
3. 效能優秀
個人 VPS 資源有限,Actix Web 的高效能表現是一大優勢。
1. 學習曲線相對陡峭
// 錯誤處理需要較多樣板程式碼
async fn create_post(post_data: web::Json<CreatePostRequest>) -> Result<HttpResponse> {
match validate_post_data(&post_data) {
Ok(_) => {
match save_post_to_db(post_data.into_inner()).await {
Ok(post) => Ok(HttpResponse::Ok().json(post)),
Err(e) => {
log::error!("儲存文章失敗: {}", e);
Ok(HttpResponse::InternalServerError().json("儲存失敗"))
}
}
}
Err(validation_errors) => {
Ok(HttpResponse::BadRequest().json(validation_errors))
}
}
}
2. 較多的手動處理
對於個人專案,我們希望能專注於業務邏輯,而不是框架的細節。
Axum 是相對較新的框架,它對個人開發者有什麼吸引力?
1. 開發體驗極佳
use axum::{
extract::{Path, Query},
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
// 型別安全,編譯時就能發現錯誤
async fn get_post(Path(post_id): Path<i32>) -> Result<Json<Post>, AppError> {
let post = fetch_post_from_db(post_id).await?; // ? 運算子自動處理錯誤
Ok(Json(post))
}
// 查詢參數自動解析
#[derive(Deserialize)]
struct PostQuery {
tag: Option<String>,
page: Option<u32>,
}
async fn list_posts(Query(params): Query<PostQuery>) -> Json<Vec<Post>> {
let posts = fetch_posts_with_filter(params.tag, params.page.unwrap_or(1)).await;
Json(posts)
}
// 簡潔的路由定義
fn create_app() -> Router {
Router::new()
.route("/posts", get(list_posts).post(create_post))
.route("/posts/:id", get(get_post))
}
2. 錯誤處理超級友善
use axum::{response::IntoResponse, http::StatusCode};
use thiserror::Error;
// 定義一次,處處可用
#[derive(Error, Debug)]
enum AppError {
#[error("資料庫錯誤")]
Database(#[from] sqlx::Error),
#[error("文章 #{0} 不存在")]
PostNotFound(i32),
#[error("無效的請求: {0}")]
BadRequest(String),
}
// 自動轉換為 HTTP 回應
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, message) = match self {
AppError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "系統暫時無法使用"),
AppError::PostNotFound(id) => (StatusCode::NOT_FOUND, format!("找不到文章 #{}", id)),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
};
(status, message).into_response()
}
}
// 處理器變得超級簡潔
async fn get_post(Path(post_id): Path<i32>) -> Result<Json<Post>, AppError> {
let post = fetch_post_from_db(post_id)
.await?
.ok_or(AppError::PostNotFound(post_id))?;
Ok(Json(post))
}
3. 模組化設計,完美適合個人專案成長
// 輕鬆組織代碼,隨著功能增加而擴展
mod posts {
use super::*;
pub fn routes() -> Router {
Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(get_post).put(update_post).delete(delete_post))
.route("/:id/comments", get(get_comments).post(add_comment))
}
// ... 處理器實作
}
mod admin {
use super::*;
pub fn routes() -> Router {
Router::new()
.route("/posts", get(admin_list_posts))
.route("/comments/pending", get(pending_comments))
// 需要認證的管理功能
.layer(auth_middleware())
}
}
// 主應用程式
pub fn create_app() -> Router {
Router::new()
.nest("/api/posts", posts::routes())
.nest("/api/admin", admin::routes())
.route("/health", get(health_check))
}
4. 與 Tokio 生態完美整合
對個人專案來說,這意味著:
1. 相對較新
2. 需要學習 Tower 概念
讓我們看看兩個框架實現個人部落格常見功能的程式碼對比:
Actix Web 版本:
async fn get_posts(
query: web::Query<PostQuery>,
) -> Result<HttpResponse, actix_web::Error> {
let page = query.page.unwrap_or(1);
let per_page = query.per_page.unwrap_or(10).min(50);
let tag = query.tag.as_deref();
match fetch_posts(page, per_page, tag).await {
Ok(posts) => {
let response = PostListResponse {
posts,
page,
per_page,
total: count_posts(tag).await.unwrap_or(0),
};
Ok(HttpResponse::Ok().json(response))
}
Err(e) => {
log::error!("查詢文章失敗: {}", e);
Ok(HttpResponse::InternalServerError().json("查詢失敗"))
}
}
}
Axum 版本:
async fn get_posts(
Query(params): Query<PostQuery>,
) -> Result<Json<PostListResponse>, AppError> {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(10).min(50);
let posts = fetch_posts(page, per_page, params.tag.as_deref()).await?;
let total = count_posts(params.tag.as_deref()).await?;
Ok(Json(PostListResponse {
posts,
page,
per_page,
total,
}))
}
明顯可以看出 Axum 版本更簡潔,錯誤處理更優雅!
經過深入比較,我決定為我們的個人部落格選擇 Axum:
學 Axum = 學 Tokio 生態系統 = 未來 Rust 非同步開發的基石
個人時間有限,Axum 讓我們能專注於功能實現,而不是框架細節
一人開發最怕的就是線上出問題,Axum 在編譯時就能發現很多錯誤
從簡單部落格到複雜應用,Axum 的架構都能支撐
代表 Rust Web 開發的未來方向
讓我們用 Axum 建立專案骨架:
# 建立個人部落格專案
cargo new blog
cd blog
在 Cargo.toml
中添加:
[dependencies]
axum = { version = "0.8.4", features = ["macros"] }
tower = "0.5.2"
tower-http = { version = "0.6.6", features = ["cors", "trace"] }
# 非同步運行時
tokio = { version = "1.0", features = ["full"] }
# JSON 處理 - 個人部落格必備
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenvy = "0.15"
# OpenAPI / Swagger UI
utoipa = { version = "5", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
# 日期時間 - 文章發布時間
chrono = { version = "0.4", features = ["serde"] }
# 簡單的日誌
tracing = "0.1"
tracing-subscriber = "0.3"
# 錯誤處理 - 讓個人維護更輕鬆
thiserror = "2.0.16"
// src/main.rs
use axum::{routing::get, Json, Router};
use serde::Serialize;
use tokio::net::TcpListener;
use std::env;
// ---- 新增:utoipa 匯入 ----
use utoipa::{OpenApi, ToSchema};
use utoipa_swagger_ui::SwaggerUi;
#[derive(Serialize, ToSchema)]
struct BlogInfo {
name: String,
description: String,
author: String,
status: String,
}
#[utoipa::path(
get,
path = "/",
tag = "blog",
responses(
(status = 200, description = "取得部落格資訊", body = BlogInfo)
)
)]
async fn blog_info() -> Json<BlogInfo> {
Json(BlogInfo {
name: "我的個人技術部落格".to_string(),
description: "分享程式設計學習心得與生活感悟".to_string(),
author: "你的名字".to_string(),
status: "running".to_string(),
})
}
#[utoipa::path(
get,
path = "/health",
tag = "health",
responses(
(status = 200, description = "健康檢查 OK", body = String)
)
)]
async fn health_check() -> &'static str {
"個人部落格運行正常!"
}
// ---- 新增:彙整 API 文件 ----
#[derive(OpenApi)]
#[openapi(
paths(
blog_info,
health_check,
),
components(
schemas(BlogInfo)
),
tags(
(name = "blog", description = "部落格資訊相關 API"),
(name = "health", description = "服務健康檢查")
)
)]
struct ApiDoc;
#[tokio::main]
async fn main() {
// 載入 .env(沒有就略過)
let _ = dotenvy::dotenv();
// 從環境變數讀 HOST/PORT,沒有就用預設
let protocol = env::var("PROTOCOL").unwrap_or_else(|_| "http".into());
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".into());
let port = env::var("PORT").unwrap_or_else(|_| "3000".into());
let addr = format!("{host}:{port}");
// 你的原本路由
let app = Router::new()
.route("/", get(blog_info))
.route("/health", get(health_check))
.merge(
SwaggerUi::new("/docs")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
);
let listener = TcpListener::bind(&addr).await.unwrap();
println!("🚀 個人部落格服務啟動於 {protocol}://{addr} ;Swagger UI 在 {protocol}://{addr}/docs");
axum::serve(listener, app).await.unwrap();
}
注意:我們故意保持簡潔,沒有加入日誌、錯誤處理等複雜功能。這些會在接下來的日子裡逐步加入!
cargo run
測試 API:
# 查看部落格資訊
curl http://127.0.0.1:3000/
# {"name":"我的個人技術部落格","description":"分享程式設計學習心得與生活感悟",...}
# 健康檢查
curl http://127.0.0.1:3000/health
# 個人部落格運行正常!
選擇 Axum 如果你:
選擇 Actix Web 如果你:
個人建議:
對於個人部落格專案,Axum 是更好的選擇。它讓你能專注於創作內容管理的邏輯,而不是框架的複雜性。
今天我們從個人開發者的角度深入比較了兩大 Web 框架:
個人專案視角:
技術決策:
實戰準備:
準備好用 Rust 打造專屬於你的部落格平台了嗎?我們明天見!