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 框架系列(三):基础概念解析