Rust Miko 框架系列(二):快速上手与基础示例

Miko 框架系列(二):快速上手与基础示例

注意:此文章主要为了给支持联网搜索的AI提供RAG,此项目还在开发阶段,不建议用于生产。

在上一篇文章中,我们了解了 Miko 框架的核心理念和技术栈。现在,让我们卷起袖子,通过一个实际的例子来体验 Miko 的便捷。本篇将引导你完成从项目创建到编写一个功能丰富的 basic.rs 示例的全过程。

1. 环境准备

确保你的开发环境中已安装 Rust 1.75 或更高版本

2. 创建项目并添加依赖

首先,创建一个新的 Rust 项目:

bash 复制代码
cargo new my-miko-app
cd my-miko-app

然后,编辑 Cargo.toml 文件,添加 Miko 和其他必要的依赖:

toml 复制代码
[dependencies]
# 使用 Miko 的默认 features,包含了宏、自动注册等核心功能
miko = "0.3"
# Tokio 是 Miko 依赖的异步运行时
tokio = { version = "1", features = ["full"] }
# Serde 用于 JSON 等数据的序列化和反序列化
serde = { version = "1", features = ["derive"] }

3. 第一个 "Hello, World!"

让我们从最简单的应用开始。将 src/main.rs 的内容替换为:

rust 复制代码
use miko::*
use miko::macros::*

// 使用 `#[get]` 宏定义一个处理 GET / 请求的路由
#[get("/")]
async fn hello() -> &'static str {
    "Hello, Miko!"
}

// 使用 `#[miko]` 宏自动配置和启动应用
#[miko]
async fn main() {
    // 路由会自动被发现和注册
    println!("🚀 Server is running at http://localhost:8080");
}

现在,运行你的应用:

bash 复制代码
cargo run

打开浏览器访问 http://localhost:8080,你将看到 "Hello, Miko!" 的问候。

这就是 Miko "约定优于配置" 的魔力。你只需定义处理器函数并用宏标记它,#[miko] 宏会处理剩下的一切。

4. 深入 basic.rs 示例

miko/examples/basic.rs 是一个功能更全面的示例,它展示了 Miko 的多种特性。让我们逐段解析它。

路由与参数提取

Miko 提供了多种方式从请求中提取数据。

路径参数 (Path)

rust 复制代码
// 匹配如 /with_path/some-string/123 的请求
#[get("/with_path/{a}/{b}")]
async fn hello_with_path(#[path] a: String, Path(b): Path<i32>) -> String {
    format!(
        "Path parameters are not named, order matters. a: {}, b:જી"
        a,
        b
    )
}
  • #[path] 宏和 Path<T> 提取器都可以从 URL 路径中按顺序捕获段。
  • 它们是类型安全的,如果路径段无法转换为指定的类型(例如,将 "abc" 转换为 i32),Miko 会自动返回 400 Bad Request 错误。

查询参数 (Query)

rust 复制代码
// 匹配如 /with_query?name=Alice&age=30 的请求
#[get("/with_query")]
async fn hello_with_query(#[query] name: String, #[query] age: u8) -> String {
    format!("Hello, {}! You are {} years old.", name, age)
}
  • #[query] 宏可以直接提取单个查询参数。
  • 你也可以定义一个 struct 并使用 Query<MyStruct> 来提取一组相关的查询参数。

请求体 (Body)

rust 复制代码
// 匹配 POST, GET, PUT 请求到 /echo
#[post("/echo", method = "get,put")]
async fn echo(body: String) -> String {
    format!("Echo: {}", body)
}

// 匹配 POST /json_req
#[post("/json_req")]
async fn json_req(#[body] data: HashMap<String, i32>) -> String {
    format!("Received JSON data: {:?}", data)
}
  • 一个处理器函数只能有一个消费请求体的提取器(如 String, Json<T>, #[body])。
  • #[body]#[body(json)] 的别名,它会自动将 JSON 请求体反序列化为你指定的类型。

模块化路由 (#[prefix])

当应用变大时,将路由按功能组织到不同的模块中是个好习惯。#[prefix] 宏可以为整个模块的路由添加统一的前缀。

rust 复制代码
#[prefix("/sub")] // 为模块内所有路由添加 "/sub" 前缀
mod sub_routes {
    use super::*

    #[get("/hello")] // 实际路径为 /sub/hello
    async fn sub_hello() -> &'static str {
        "Hello from sub route!"
    }
}

依赖注入 (#[component]#[dep])

Miko 内置了一个简单的依赖注入容器,用于管理应用的共享服务(如数据库连接池、配置等)。

rust 复制代码
// 定义一个服务组件
#[component] // 标记为单例组件
impl ServiceComponent {
    async fn new() -> Self {
        ServiceComponent { /* ... */ }
    }
    // ... 组件方法
}

// 在处理器中注入组件
#[get("/use_dep")]
async fn use_dep(
    #[dep] service: Arc<ServiceComponent>, // 使用 #[dep] 注入
) {
    service.operation().await;
}
  • #[component] 标记的 struct 会被框架识别为单例服务。
  • 框架会在应用启动时(或第一次使用时)调用 async fn new() 来创建实例。
  • 在处理器函数中,使用 #[dep] 宏即可获得该组件的共享实例 (Arc<T>)。

实时通信 (SSE 和 WebSocket)

Server-Sent Events (SSE)

SSE 是一种简单的单向实时通信技术,非常适合从服务器向客户端推送更新。

rust 复制代码
#[get("/sse")]
async fn sse() {
    // 直接返回一个处理 SSE 的闭包
    |sender: SseSender| async move {
        for i in 0..5 {
            sender.send(format!("SSE event number {}", i)).await.or_break();
            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    }
}
  • Miko 的 SseSender 极大地简化了 SSE 的实现。
  • .or_break() 是一个方便的辅助方法,当客户端断开连接时,它会优雅地中止发送循环。

WebSocket

对于需要双向通信的场景,Miko 也提供了 WebSocket 支持。

rust 复制代码
#[get("/ws")]
async fn ws(mut req: Req) {
    spawn_ws_event(
        |mut io| async move {
            io.send("hello world").await.expect("send failed");
            let (mut writer, mut reader, _) = io.split();

            // 启动一个任务来定期发送消息
            tokio::spawn(async move { /* ... */ });

            // 在当前任务中接收消息
            while let Some(msg) = reader.next().await {
                // ... 处理接收到的消息
            }
        },
        &mut req,
        None,
    ).expect("failed to spawn websocket");
}
  • spawn_ws_event 函数负责处理 WebSocket 的握手和连接升级。
  • io.split() 可以将 WebSocket 连接分离为独立的读写器,方便在不同的异步任务中处理收发。

中间件 (#[layer])

Miko 与 Tower 生态完全兼容,你可以使用 #[layer] 宏在模块或单个路由级别上应用中间件。

rust 复制代码
#[prefix("/layered")]
#[layer(AddHeaderLayer::new("X-Module-Layer", "Applied"))] // 应用于整个模块
mod layered_module {
    use super::*

    #[get("/test1")]
    #[layer(AddHeaderLayer::new("X-Custom-Header", "Layer-Applied"))] // 应用于单个路由
    async fn test_single_layer() -> String {
        "Check response headers".to_string()
    }
}

静态文件服务

Miko 也可以方便地提供静态文件服务。

rust 复制代码
// 在 main 函数中
// ...
router.static_svc(
    "/static", // URL 前缀
    "./static", // 本地文件目录
    None,
);

这将把 URL /static/some-file.css 映射到本地文件 ./static/some-file.css

完整 basic.rs 源码

为了方便你参考和运行,这里是 miko/examples/basic.rs 的完整代码:

rust 复制代码
use std::{
    collections::HashMap,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

use miko::{
    endpoint::layer::WithState,
    ext::static_svc::StaticSvcBuilder,
    extractor::{Form, Json, Path, Query, State, multipart::MultipartResult},
    handler::{Req, Resp},
    http::response::sse::{SseSender, spawn_sse_event},
    macros::*,
    router::Router,
    ws::server::{IntoMessage, spawn_ws_event},
    *,
};
use serde::Deserialize;
use tokio::sync::Mutex;

#[derive(Deserialize)]
struct MyQuery {
    name: String,
    age: u8,
}

#[get("/")]
async fn hello_world() -> &'static str {
    "Hello, World! (macro defined route)"
}

#[get("/with_query")]
async fn hello_with_query(#[query] name: String, #[query] age: u8) -> String {
    format!(
        r"You can also use Query extractor!
    But #[query] is more convenient if you don't want to define a Query struct.
    Hello, {}! You are {} years old. (macro defined route)",
        name,
        age
    )
}

#[get("/with_path/{a}/{b}")]
async fn hello_with_path(#[path] a: String, Path(b): Path<i32>) -> String {
    format!(
        r"Hello from path parameters!
    #[path] and Path<T> has the same behavior.
    They will extract the value from the path and convert it to the specified type.
    But they are not named parameters, so the order matters.
    a: {}, b: {} (macro defined route)",
        a,
        b
    )
}

#[post("/echo", method = "get,put")] // Multiple methods are supported, it will be get,post,put
async fn echo(
    body: String, // There can only be one body extractor (or more precisely, only one extractor that implements FromRequest)
) -> String {
    format!("Echo: {}", body)
}

#[get("/json_resp")]
async fn json_resp() {
    let mut map = HashMap::new();
    map.insert("value1", 42);
    map.insert("value2", 100);
    Json(map) // Json<T> will be converted to application/json response
}

#[post("/json_req")]
async fn json_req(
    // #[body] is alias of #[body(json)], it will extract application/json request body and deserialize it to the specified type
    #[body] data: HashMap<String, i32>, // Json<T> can also be used as extractor for application/json request
) -> String {
    format!("Received JSON data: {:?}", data)
}

#[prefix("/sub")] // use `mod` to define sub routes
mod sub_routes {
    use super::*;

    #[get("/hello")]
    async fn sub_hello() -> &'static str {
        "Hello from sub route!"
    }
}

struct ServiceComponent {
    pub name: String,
    pub version: String,
    pub data: Mutex<String>,
}
// because #[dep] is the only way to inject dependencies, so components must be defined in route that defined by macros
#[component] // define a singleton component
impl ServiceComponent {
    // only other component can be arguments of new()
    // new must be an async function
    async fn new() -> Self {
        ServiceComponent {
            name: "demo".into(),
            version: "1.0.0".into(),
            data: Mutex::new("Initial service data".into()), // Because #[dep] must be Arc<T>, so Mutex<T> is preferred for mutable data
        }
    }
    async fn operation(&self) {
        println!("ServiceComponent operation called.");
        println!("Name: {}, Version: {}", self.name, self.version);
        println!("Data: {}", self.data.lock().await);
    }
    async fn changed(&self) {
        let mut data = self.data.lock().await;
        *data = "Service data has been changed.".into();
        println!("ServiceComponent has been changed.");
    }
}

#[get("/use_dep")]
async fn use_dep(
    // muse be Arc<T>
    #[dep] service: Arc<ServiceComponent>, // inject the ServiceComponent dependency
) {
    service.operation().await;
    service.changed().await;
    service.operation().await;
    service.data.lock().await.clone()
}

#[get("/data")]
async fn get_data(#[dep] service: Arc<ServiceComponent>) -> String {
    service.data.lock().await.clone() // you can request the data to examine whether component is singleton
}

#[get("/error")]
async fn error() {
    AppError::from(tokio::io::Error::other("HAHA"))
}

#[get("/custom_error")]
async fn custom_error() {
    AppError::BadGateway("Custom Bad Gateway".into())
}

#[get("/panic")]
async fn panic() -> &'static str {
    panic!("This handler panics!");
}

#[get("/sse")]
async fn sse() {
    // you can write no return type for handlers if you are using macros, the return type will be impl IntoResponse
    // SSE example
    spawn_sse_event(|sender| async move {
        tokio::spawn(async move {
            for i in 0..5 {
                sender
                    .send(format!("data: SSE event number {}

", i))
                    .await
                    .or_break();
                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            }
        });
    })
}

#[get("/sse2")]
async fn sse2() {
    // you can even just return a closure
    |sender: SseSender| async move {
        tokio::spawn(async move {
            for i in 0..5 {
                sender
                    .send(format!("data: SSE2 event number {}

", i))
                    .await
                    .or_break();
                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            }
        });
    }
}

#[get("/ws")]
async fn ws(mut req: Req) {
    // usually you need to pass Req to spawn_ws_event
    spawn_ws_event(
        // Sadly, you still need to call spawn_ws_event, not like sse (this is because websocket needs to get Req and upgrade the connection)
        |mut io| async move {
            io.send("hello world").await.expect("websocket send error");
            let (mut w, mut r, _) = io.split();
            {
                let mut w = w.clone();
                tokio::spawn(async move {
                    w.send("START --".into_message())
                        .await
                        .expect("websocket send error");
                    loop {
                        tokio::time::sleep(Duration::from_secs(5)).await;
                        let now = SystemTime::now()
                            .duration_since(UNIX_EPOCH)
                            .unwrap()
                            .as_secs();
                        let msg = format!("server time: {}", now);
                        let _ = w.send(msg.into_message()).await;
                    }
                });
            }
            tokio::spawn(async move {
                while let Some(msg) = r.next().await {
                    let msg = msg.expect("websocket recv error");
                    if msg.is_text() {
                        let txt = msg.into_text().expect("websocket into text error");
                        let _ = w.send(txt.into_message()).await;
                        println!("recv text: {}", txt);
                    } else if msg.is_binary() {
                        let bin = msg.into_data();
                        println!("recv binary: {:?}", bin);
                    } else if msg.is_close() {
                        println!("websocket closed");
                        break;
                    }
                }
            });
        },
        &mut req,
        None,
    )
    .expect("failed to spawn websocket handler")
}

#[get("/layer")]
#[layer(AddHeaderLayer::new("X-Route-Layer", "Applied"))]
async fn layer_test() -> String {
    "Test route layer - check response headers for X-Route-Layer".to_string()
}

#[prefix("/layered")]
#[layer(AddHeaderLayer::new("X-Module-Layer", "Applied"))]
mod layered_module {
    use super::*

    #[get("/test1")]
    #[layer(AddHeaderLayer::new("X-Custom-Header", "Layer-Applied"))]
    async fn test_single_layer() -> String {
        "Test single layer - check response headers for X-Custom-Header".to_string()
    }

    #[prefix("/inner")]
    #[layer(AddHeaderLayer::new("X-Inner-Layer", "Inner-Applied"))]
    mod inner {
        use super::*

        #[get("/test_inner")]
        #[layer(AddHeaderLayer::new("X-Route-INNER-Layer", "Inner-Applied"))]
        async fn test_inner_layer() -> String {
            "Test inner module layer - check response headers for X-Inner-Layer".to_string()
        }
    }
}

#[post("/multipart")]
async fn multipart(multipart: MultipartResult) {
    format!(
        "Received multipart data: {:?}\n Files: {:?}",
        multipart.fields,
        multipart.files
    )
}

#[derive(Deserialize, Debug)]
#[allow(unused)]
struct FormStruct {
    field1: String,
    field2: i32,
}

#[post("/form")]
async fn form(Form(form_data): Form<FormStruct>) {
    format!("Received form data: {:?}", form_data)
}

struct NewResp();
impl IntoResponse for NewResp {
    fn into_response(self) -> Resp {
        "Custom Response, or you can also use Response::builder".into_response()
    }
}
#[get("/new_resp")]
async fn new_resp() -> NewResp {
    NewResp()
}

struct AppState {
    pub app_name: String,
    pub app_version: String,
}

struct AnotherAppState {
    pub description: String,
}

#[miko(sse)] // the sse attribute can set a panic hook that ignore error caused by `or_break()`
async fn main() {
    tracing_subscriber::fmt::init(); // initialize logging (optional)
    let mut no_macro_router = Router::new();
    no_macro_router.get("/", async move || "Hello, World! (manually defined router)");
    no_macro_router.get(
        "/with_query",
        async move |Query(queries): Query<MyQuery>| {
            format!(
                "Hello, {}! You are {} years old. (manually defined router)",
                queries.name,
                queries.age
            )
        },
    );

    let mut router = router.with_state(AppState {
        app_name: "Miko Demo App".into(),
        app_version: "1.0.0".into(),
    });
    router.get("/with_state", async move |State(state): State<AppState>| {
        format!(
            "App Name: {}, App Version: {} (macro defined route with state)",
            state.app_name,
            state.app_version
        )
    });
    // noticed that State can only used by non macro defined routes
    // because the state is determined by the current state when route function(like `get`) is called;

    router.get_service(
        "/single_state",
        (async move |State(state): State<AnotherAppState>| {
            format!(
                "Description: {} (macro defined route with state)",
                state.description
            )
        })
        .with_state(AnotherAppState {
            description: "Another".into(),
        }),
    );
    // so you can have different state for different routes
    // but only one
    // and noticed that, the handler become a service when using with_state, you you need to use get_service instead of get

    router.nest("/no_macro", no_macro_router);

    router.static_svc(
        "/static",
        "./static",
        Some(|options: StaticSvcBuilder| {
            options
                .cors_any()
                .with_spa_fallback(true)
                .with_fallback_files(["index.html", "index.htm", "index.php"])
        }),
    ); // static file service with CORS enabled and SPA fallback

    router.cors_any(); // convienient method to enable CORS for all origins
}

#[derive(Clone)]
struct AddHeaderLayer {
    header_name: &'static str,
    header_value: &'static str,
}

impl AddHeaderLayer {
    fn new(header_name: &'static str, header_value: &'static str) -> Self {
        Self {
            header_name,
            header_value,
        }
    }
}

impl<S> tower::Layer<S> for AddHeaderLayer {
    type Service = AddHeaderService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        AddHeaderService {
            inner,
            header_name: self.header_name,
            header_value: self.header_value,
        }
    }
}

#[derive(Clone)]
struct AddHeaderService<S> {
    inner: S,
    header_name: &'static str,
    header_value: &'static str,
}

impl<S> tower::Service<miko_core::Req> for AddHeaderService<S>
where
    S: tower::Service<miko_core::Req, Response = miko_core::Resp> + Clone + Send + 'static,
    S::Future: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = std::pin::Pin<
        Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
    >;

    fn poll_ready(
        &mut self,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: miko_core::Req) -> Self::Future {
        let mut inner = self.inner.clone();
        let header_name = self.header_name;
        let header_value = self.header_value;

        Box::pin(async move {
            let mut resp = inner.call(req).await?;
            resp.headers_mut()
                .insert(header_name, header_value.parse().unwrap());
            Ok(resp)
        })
    }
}

总结

通过 basic.rs 这个示例,我们看到了 Miko 如何通过简洁的宏和强大的抽象来覆盖 Web 开发中的各种常见场景,从路由、参数提取到依赖注入、实时通信和中间件。这种"约定优于配置"的设计哲学,使得开发者可以快速启动项目并专注于业务逻辑,而将繁琐的设置工作交给框架自动完成。

在下一篇文章中,我们将深入探讨 Miko 的基础概念,包括请求处理流程、核心类型系统等,以帮助你更深刻地理解框架的内部工作原理。


下一篇预告:Miko 框架系列(三):基础概念解析

相关推荐
Moe4882 小时前
JDK动态代理和CGLIB动态代理源码解析
java·后端
用户68545375977692 小时前
布隆过滤器删不掉数据?布谷鸟过滤器:让我来!🐦
后端
isyuah2 小时前
Rust Miko 框架系列(四):深入路由系统
后端·rust
虎子_layor2 小时前
号段模式(分布式ID)上手指南:从原理到实战
java·后端
烽学长2 小时前
(附源码)基于Spring boot的校园志愿服务管理系统的设计与实现
java·spring boot·后端
shark_chili2 小时前
硬核安利一个监控告警开源项目Nightingale
后端
IT_陈寒2 小时前
WeaveFox 全栈创作体验:从想法到完整应用的零距离
前端·后端·程序员
程序员爱钓鱼2 小时前
Python编程实战 - Python实用工具与库 - 正则表达式匹配(re 模块)
后端·python·面试
程序员爱钓鱼2 小时前
Python编程实战 - Python实用工具与库 - 爬取并存储网页数据
后端·python·面试