Hotaru:一个理念构建的 Rust Web 框架 — 端点的一切都应该在一起

1 月 6 日
 JerrySu5379

Hotaru:一个理念构建的 Rust Web 框架 — 端点的一切都应该在一起

Hotaru 是一个采用宏语法的 Rust Web 框架,将 URL 、中间件、配置和处理器整合在一个代码块中。如果你正在用 Rust 构建 Web 服务,并且觉得属性宏分散各处不够优雅,这个框架可能适合你。希望获得反馈:endpoint!/middleware! 语法是否直观,还是隐藏了太多细节?

仓库地址: https://github.com/Field-of-Dreams-Studio/hotaru

文档: https://fds.rs/hotaru/tutorial/0.7.3/


为什么我们要构建 Hotaru

我是从 Python 转过来的,之前用 Flask 、FastAPI 那一套。当我转向 Rust 时,安全性的承诺打动了我——内存安全、没有空指针异常、编译器在运行前就能捕获 bug 。这些确实兑现了。

但当我开始研究 Web 框架时,发现了一个让我不太满意的模式:

#[get("/users/<id>")]
#[middleware::auth]
#[middleware::rate_limit(100)]
async fn get_user(...) -> impl Responder {

属性宏这种方式确实可行,很多人用它来交付生产环境的应用。但对我个人而言,配置分散在函数上方的感觉和我之前想要摆脱的装饰器模式很像。而且你还需要在别的地方手动注册路由。我想要的是能在一个地方看到端点的所有信息,并且自动完成注册。

所以我们尝试了一种不同的方式。


Hotaru 的设计理念

我们围绕一个核心理念构建了 Hotaru:端点的所有信息都应该在一起,并且自动注册。URL 、中间件、配置、处理器——一个代码块,一目了然,定义即注册。

基础语法

endpoint! {
    APP.url("/users/<int:id>"),
    middleware = [.., auth_check, rate_limit],
    config = [HttpSafety::new().with_allowed_methods(vec![GET, POST])],

    pub get_user <HTTP> {
        let user_id = req.param("id").unwrap_or_default();
        json_response(object!({
            id: user_id,
            message: "User endpoint"
        }))
    }
}

这就是完整的语法。URL 模式(支持类型化参数如 <int:id>)、中间件栈、安全配置和处理器主体——全部在一个地方。req.param("id") 返回的值可以调用 .unwrap_or_default().string() 获取原始字符串。无需单独的注册步骤。宏在编译时展开为标准的异步 Rust 代码。

新语法试验中

注意: 我们正在测试一种新的语法风格,可以使用更接近 Rust 函数的写法并自定义请求变量名。这个功能还在试验阶段,欢迎反馈。

endpoint! {
    APP.url("/new_syntax/<arg>"),
    middleware: [..],
    config: ["ConfigString"],

    pub fn new_syntax_endpoint(ctx: HTTP) {
        let arg = ctx.pattern("arg").unwrap_or_default();
        text_response(format!("New syntax endpoint called with arg: {}", arg))
    }
}

两种写法在底层编译为相同的代码。我们想知道:同时支持两种风格是提高了清晰度,还是引入了不必要的不一致性?


核心特性

1. 自动注册

定义端点的同时自动注册。不需要手动 router.register(),不会忘记注册,也不用到处找路由在哪里组装的。这是大多数 Rust Web 框架缺少的功能。

2. 中间件定义

在大多数框架中定义中间件需要实现 trait 、包装服务、处理返回 future 的 future 。相当繁琐。

在 Hotaru 中:

middleware! {
    pub LogRequest <HTTP> {
        println!("[LOG] {} {}", req.method(), req.path());
        let start = std::time::Instant::now();

        let result = next(req).await;

        println!("[LOG] Completed in {:?}", start.elapsed());
        result
    }
}

就这样。你拿到 req(即 HttpContext),调用 next(req).await 继续执行链,可以在返回时修改结果。想要短路?不调用 next() 就行:

middleware! {
    pub AuthCheck <HTTP> {
        let token = req.headers().get("Authorization");

        if token.is_none() {
            req.response = json_response(object!({
                error: "unauthorized"
            })).status(StatusCode::UNAUTHORIZED);
            return req;
        }

        // 通过 locals 向下游传递类型化数据
        req.locals.set("user_id", "user-123".to_string());
        next(req).await
    }
}

新的 fn 风格也支持中间件(试验中):

middleware! {
    pub fn Logger(req: HTTP) {
        println!("[LOG] {} {}", req.method(), req.path());
        next(req).await
    }
}

3. .. 模式

这里我们借鉴了 Rust 结构体更新语法的设计。在大多数框架中,中间件组合要么全有要么全无,要么需要在构建器链中仔细排序。

// 应用级全局中间件
pub static APP: SApp = Lazy::new(|| {
    App::new()
        .binding("127.0.0.1:3000")
        .append_middleware::<Logger>()
        .append_middleware::<Metrics>()
        .build()
});

// 仅使用全局中间件
endpoint! {
    APP.url("/health"),
    middleware = [..],
    pub health <HTTP> { text_response("ok") }
}

// 全局 + 认证
endpoint! {
    APP.url("/api/users"),
    middleware = [.., auth_required],
    pub users <HTTP> { /* ... */ }
}

// 三明治结构:timing 先执行,然后是全局中间件,最后是缓存检查
endpoint! {
    APP.url("/api/cached"),
    middleware = [timing, .., cache_layer],
    pub cached <HTTP> { /* ... */ }
}

// 完全跳过全局中间件
endpoint! {
    APP.url("/raw"),
    middleware = [custom_only],
    pub raw <HTTP> { /* ... */ }
}

.. 会展开为你的全局中间件。你可以在它前面、后面添加内容,或者完全跳过。只需查看端点定义,就能准确知道每个路由会执行哪些中间件。

4. 单端口多协议

这最初是一个实验,后来成为了架构的核心。Hotaru 可以在同一个端口上提供 HTTP 、WebSocket 和自定义 TCP 协议服务。

(我们实际上正在将这部分封装成更简洁的宏——新语法即将推出)

pub static APP: SApp = Lazy::new(|| {
    App::new()
        .binding("127.0.0.1:3000")
        .handle(
            HandlerBuilder::new()
                .protocol(ProtocolBuilder::new(HTTP::server(HttpSafety::default())))
                .protocol(ProtocolBuilder::new(WebSocketProtocol::new()))
                .protocol(ProtocolBuilder::new(CustomProtocol::new()))
        )
        .build()
});

框架会检查传入的字节并路由到正确的处理器。REST API 、WebSocket 、自定义二进制协议——同一端口,共享状态。

无论什么协议,处理器看起来都一样:

endpoint! {
    APP.url("/chat"),
    pub chat_http <HTTP> {
        html_response(include_str!("chat.html"))
    }
}

endpoint! {
    APP.url("/chat"),
    pub chat_ws <WebSocket> {
        // 相同 URL ,不同协议
        ws.on_message(|msg| { /* ... */ }).await
    }
}

5. Akari:我们的辅助 Crate

我们提供了一个轻量级工具 crate 叫 Akari ,用于处理 JSON 和模板,无需引入 serde 。object! 宏可以内联构建 JSON:

json_response(object!({
    status: "success",
    data: {
        users: [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}],
        total: 2
    }
}))

不需要派生宏,不需要为临时响应定义结构体。如果你想用 serde 也可以——不是禁止使用,只是不强制要求。


早期性能数据

我们进行了一些初步基准测试,以验证宏方法不会增加运行时开销。这些是单机上的早期数据——仅供参考方向,不作为定论。

框架 请求数/秒 (JSON) 相对性能
Hotaru 173,254 100%
Rocket 171,904 99.2%
Actix-web 149,244 86.1%
Axum 148,934 86.0%

在 Apple M 系列芯片上测试,单线程,简单 JSON 响应。我们将很快发布完整的测试方法和代码。

宏展开后就是你手写的普通异步函数。编译器看到的是展开后的普通 Rust 代码,并相应地进行优化。


设计理念:为什么用宏?

传统属性宏的问题

在大多数 Rust Web 框架中,端点配置是这样的:

#[get("/users/<id>")]
#[middleware::auth]
#[middleware::rate_limit(100)]
async fn get_user(...) -> impl Responder {
    // 处理器代码
}

// 然后在别的地方,你还需要注册它:
// router.register(get_user)

配置分散在函数上方。注册在别的地方。要理解一个端点做什么——以及它是否真的生效——你需要检查多个地方。

我们的方法

一个代码块。一个地方。URL 、中间件、配置、处理器——以及自动注册

核心特性

  1. 自动注册:定义端点的同时就注册了。不需要手动 router.register(),不会忘记注册,不用到处找路由在哪里组装的。这是大多数 Rust Web 框架缺少的功能。

  2. 一眼看清:端点的所有信息都在一个地方可见。

  3. 中间件组合:借鉴 Rust 的结构体更新语法——准确看到每个路由运行哪些中间件。

  4. 简化的中间件定义:没有 trait 样板代码。

  5. 多协议支持:相同 URL ,不同协议——语法一致。

权衡与路线图

我们承认当前的局限性:

局限性 状态
自定义语法有学习曲线 编译为标准异步 Rust
rustfmt 支持有限 计划中:自定义格式化工具
IDE 支持参差不齐 已针对 rust-analyzer 优化

我们正在构建一个完整的工具链——包括我们自己的格式化工具和增强的 IDE 支持。Hotaru 正在从一个框架演变为一个带有完整工具的 Web 端点 DSL 。

编译时保证

所有宏都在编译时展开。零运行时开销——编译器在展开后看到的是普通 Rust 。


当前状态

我们目前是 v0.7 版本,还在起步阶段。API 正在趋于稳定但还没有冻结。文档在改进中但仍有缺口。与成熟框架相比,生态系统还很小。

关于测试: 由于我们的数据库实现仍然非常抽象,目前的基准测试只能覆盖基础的 HTTP 响应场景。我们正在完善数据库层的设计,之后会进行更全面的测试。

我们的定位是一个对语法有独特见解的框架。我们认为处理器代码应该读起来就像它做的事情一样直观。


试试看

use hotaru::prelude::*;
use hotaru::http::*;

pub static APP: SApp = Lazy::new(|| {
    App::new().binding("127.0.0.1:3000").build()
});

endpoint! {
    APP.url("/"),
    pub index <HTTP> {
        text_response("Hello from Hotaru")
    }
}

#[tokio::main]
async fn main() {
    APP.clone().run().await;
}

仓库地址: https://github.com/Field-of-Dreams-Studio/hotaru

文档: https://fds.rs/hotaru/tutorial/0.7.3/


我们想听听你的看法

我们押注的是:用一点宏魔法换取更少的样板代码和自动注册是值得的。


贡献

我们是一个小团队,正在构建一些有野心的东西。如果你对以下方面感兴趣:

我们很乐意听到你的声音。访问我们的 GitHub 或者提 issue 开始对话。

1366 次点击
所在节点    分享创造
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1183437

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX