Rust Web开发不为人知的另一面
[LOADING...]
本文是 Mateusz Maćkowski 与 Marek Grzelak 的客座文章,他们是 cot.rs 的联合维护者,也是 Rustikon 2026 的演讲者。你可以在此观看完整演讲。
最初,我们只是想构建一个 JSON API。在 Rust 中重复做了几次之后,我们发现一个反复出现的模式:每个新项目都意味着要选择库、拼接所有组件、编写相同的胶水代码,并再次解决相同的配置问题。
正是这种模式促使我们开始开发 cot.rs。我们希望 Rust Web 开发不再像每次都要组装定制工具箱,而是从那些你知道自己迟早会用到的组件开始。
Rust 的优势众所周知:安全性、高性能、强类型,以及代码最终编译通过时那种独特的安心感。而本文要聊的,是编译之前的所有那些事。
TL;DR
- 异步 Rust 功能强大,但调试体验仍然粗糙。一次
panic!可能带来 100 帧的回溯,而实际代码中的问题出现在第 9 和第 10 帧之间。 - Rust ORM 要求你在多个地方维护相同的模式。声明式迁移是一个更好的方向,多个项目正在探索这一点。
- Rust Web 框架中的错误处理不一致。让错误在整个应用中表现得可预测,比听起来要难得多。
- 宏在 Rust Web 技术栈中无处不在。好用的时候还好,不好用的时候,你就要去读数千行生成的代码。
- 编译时间对 Web 开发而言是一个实际成本。仅一个 Web 框架依赖就能让你的依赖树膨胀十倍。
- 生态系统碎片化。你几乎需要自己选择技术栈的每一个部分,这对有经验的开发者来说很强大,但对其他人来说则令人不知所措。
- 像 Loco.rs 和 cot.rs 这样“开箱即用”的框架正在缩小差距,但我们还没有达到 Django 或 Rails 的水平。
异步:快速、巧妙,但并非总是友好
第一个问题,也是目前最突出的问题,就是异步。
Rust 的异步模型确实很精巧。它以一种非常聪明的方式实现,明显是聪明人做出来的。不过,尽管它在技术上令人印象深刻,但开发体验还远未成熟。
要舒适地使用它,你既需要理解 async fn,也需要知道如何手动实现 Future,而这两者差异很大,感觉像在学习两样不同的东西。然后是任务(task)与线程(thread)的抉择,如果你搞混了并用错了数据结构,就可能出现死锁或其他更微妙的问题。稳定版 Rust 中仍然没有 async drop。如果你曾试图真正理解 pinning,你会发现想通它为什么有必要存在真的很困难。
调试时,这一切就变得非常明显。当你在异步函数中 yield 时,执行权返回给运行时,当它恢复执行你的函数时,回溯(backtrace)基本上被重置了。这会产生既庞大又难以使用的回溯,不仅给调试带来麻烦,也对日志记录、跟踪和学习造成了影响。
以下是我们自己碰到的一个回溯对应的代码: [LOADING...]
一行代码。回溯却跑出了 100 帧,大部分是异步运行时的内部机制。实际程序的问题——我们代码中的一个 bug,出现在第 9 帧和第 10 帧之间。技术上没错,但实际帮助不大。
这还只是开始,你还会遇到诸如为什么你的 future 不是 Send,或者某个 future 为什么会消耗意料之外的大量内存等问题。异步 Rust 提供了高性能和灵活性,但具体到 Web 开发,它把简单的事情变得比需要的更复杂。Rust 社区中有积极的行动在改善这一点,但目前的状况仍有改进空间。如果你想深入了解异步生态系统的未来走向,这篇与 Tokio 创建者 Carl Lerche 的对话值得一读。
数据库访问:写一次模式,然后再写一次
下一个痛点是与数据库的交互。
Rust 有很棒的数据库操作库,但工作流可能比应有的更手动。在 Diesel 中,你在 Rust 中定义模型,编写 SQL 迁移,还要维护一个自动生成的 schema.rs 文件。同一个数据有三次不同的表示。是的,Diesel 仍然使用原始 SQL 迁移,这就是为什么存在像 diesel-guard 这样的工具——当迁移的写法可能在大表上引发问题时,它会向你发出警告。
SeaORM 改善了部分体验,但迁移仍然感觉像用 Rust 语法在 SQL 之上再写一层。这是生态系统中普遍的模式:专门的查询 DSL,每一个都在创造自己的小语言。我们想编写现代 Web 应用,结果却感觉像在搞 2005 年的 PHP 项目。
问题不在于 SQL 存在,而在于围绕它累积了多少重复工作。如果我们已经要为查询创建另一层抽象,那它至少应该是可读的。Django 在 20 多年前就搞明白了这一点。你不应该只是为了修改一个模式而手动编写迁移。
对比一下针对同一个查询的两种方式: [LOADING...]
对比 [LOADING...]
你读代码的时间远多于写代码的时间。如果我们要发明另一种语言,至少让它可读性好一些。
同样的原则也适用于迁移。在 cot.rs 中,我们正在探索声明式迁移(declarative migration),即将迁移表示为框架层面的一种结构化操作,而不是你手动编写和维护的原始 SQL: [LOADING...]
框架不再直接写 SQL,而是将变更表示为一个操作,进行验证,然后自己生成 SQL。像那种在大表上执行时可能非常慢的迁移,可以在框架层被捕获并处理,而不需要依赖单独的 lint 工具。
我们不是唯一这么想的团队。像 toasty 和 rorm 的项目也在尝试类似的方法。Rust 中的数据库访问不需要变得神奇,它只是不应该让你感觉像是在三种稍有不同的话里把同一件事情写三遍。
错误处理:返回一个错误很容易,返回正确的错误更难
在 Web 框架中进行错误处理听起来很简单,直到你开始在意细节。
一个好的错误处理方案需要同时做到几件事:易于编写和注册、能访问尽可能多的上下文、在整个应用中保持一致、以及像其他任何响应一样经过相同的中间件。这是一组出奇难组合在一起的要求。
在 Axum 中,有两种不同的机制将错误转换为响应:IntoResponse 和 HandleErrorLayer。这意味着当你刚开始时,并不清楚到底该用哪一个。IntoResponse 要求你为每个错误类型都实现它,因此不同库中的不同错误类型最终可能返回不同的响应形状。一致性很难保证。
Actix-web 采用了不同的方法,即 ResponseError trait,这在某些方面更清晰。但它只能访问错误对象本身,无法访问构建有用响应所需的更广泛的请求上下文。
中间件则增加了另一层复杂性。如果你的应用对响应进行压缩、添加头信息或进行其他处理,你希望错误响应也走相同的路径。tower-http 生态系统在这方面提供了很多,但它设计上会消费请求,这意味着当你在处理错误时,可能已经无法访问所需的上下文了。
Rust 非常擅长让错误显式化。对于 Web 开发来说,更难的部分是让错误变得一致、可预测,并与应用栈的其他部分良好集成。
元编程:有用的魔法仍然是魔法
宏是 Rust Web 框架用起来还算舒服的原因之一。它们处理样板代码、提供强大的功能、让 API 感觉比本来更干净。路由、序列化、提取器、数据库查询、框架初始化……宏在 Rust Web 技术栈中无处不在。
好用的时候,它们很棒。出问题的时候,它们就成了黑盒子。除非你深入到生成的代码中,否则很难理解哪里出了问题。来自宏展开失败的错误消息通常指向生成代码内部的某个位置,而不是你实际做错的地方。而且如果宏生成了很多代码,找出问题就变成了一项真正的任务。
IDE 对过程宏的支持也参差不齐。生成那么多代码本身就不简单,并非所有 IDE 都能同样好地处理它。
还有泛型。它们帮助我们构建可组合、可复用的软件。但在 Web 框架中,你有很多层堆叠在一起:中间件、提取器、序列化器、请求本身。我们发现,一旦所有东西都编译完成,仅靠泛型就能把 release 模式的二进制文件从大约 2MB 推高到 37MB。单态化让你的程序在运行时很快,但也给编译器带来了更多的工作。
问题不在于宏或泛型本身不好。而在于 Rust Web 开发倾向于在彼此之上叠加许多强大的抽象,到一定程度后,理解这整个栈就成了工作的一部分。
迭代速度:“我改了个字符串,一分钟后再见”
所有这些抽象都有代价,当你试图快速推进时,这种代价变得非常明显。
Web 开发是一个紧密的循环:改代码、运行、看效果、再改。这个循环需要快。但在 Rust 中,它通常不快,特别是在 Web 应用里,问题会累加,因为你把几样各自都会让编译器更努力工作的东西组合在了一起。
泛型带来的单态化会花时间。依赖增长很快:添加一个 Web 框架 crate 就能让你的依赖树扩大十倍。宏本身也有代价。
如果你想要一个具体的例子来说明这能有多糟糕,有一个记录在案的分析发现,在宏密集的 SQLx 项目中,expand_crate 占了总编译时间的 67.5%。SQLx 在编译时进行的 SQL 验证确实有用,但它有实际的成本。
能加速吗?可以,但只能加速一点点。你可以在开发构建中禁用优化、使用替代编译器后端、采用更快的链接器。你也可以在性能可接受的场合优先使用动态分发而不是泛型。代码编译会更快,但会牺牲一些编译时的安全保证。热重载也在探索中;Dioxus 团队的 subsecond 就是尝试解决这个问题的项目之一,我们也在实验它。
但这些方法都无法让问题消失。目前,Rust Web 开发要求你接受一个比很多 Web 开发者习惯的更慢的反馈循环。这种摩擦很少只存在于一个地方。它是异步、宏、泛型和依赖在你试图构建完整应用时累积在一起的结果。
生态系统碎片化:一切都要自己选
这就引出了让其他所有事情都更难驾驭的部分。当你开始一个 Rust Web 项目时,技术栈由你自己组装。你要选 Web 框架、数据库层、迁移方式、模板引擎、前端集成方式以及认证方法。每个环节都有几个选项,而且它们之间并不总是能干净地组合在一起。
arewewebyet.org 项目总结得很好。这个页面大约五年没有更新了,但重点基本仍然成立:Rust 没有一个像 Django 或 Rails 那样占主导地位的全功能框架。大多数 Rust Web 框架更小、更模块化,精神上更接近 Flask 或 Sinatra。生态系统是多样化的,但通常你必须自己把所有东西连接起来。
对于已经拥有首选技术栈的有经验的 Rust 开发者来说,这种灵活性通常是受欢迎的。但对于较新的开发者,或者只想开始构建的团队来说,在写一行业务逻辑之前,这可能就足以让人不知所措。
这正是“开箱即用”框架的动机所在。Loco.rs 是其中最成熟的,已被许多团队用于生产环境。我们正在开发 cot.rs,目标类似,即缩小差距,为开发者提供一个更完整的起点。Roadster 是这个领域的另一个项目,不过知名度较低,我们也不太确定它在生产环境中被使用了多少。
这些项目都没有解决我们描述的所有问题。异步仍然是异步。编译时间仍然重要。宏仍然需要理解。但一个更连贯的起点至少消除了在实际应用之前的一些重复工作。
那么,这一切值得吗?
2026 年的 Rust Web 开发比以往任何时候都要好,但它仍然不是开发效率最高的生态系统。编译器在你发布之前就能捕获大多数 bug,在生产环境中这一点很重要。这种权衡是否值得,归结为一个老实的提问:你的 bug 到底有多昂贵?
如果可靠性和性能是核心要求,那么 Rust 的缺点可能是值得的。如果你需要在一个简单的东西上快速推进,Python 仍然能让你更快地到达目标。只是要知道其中的区别。
我们还没有完全到达那里。但我们正在接近。
常见问题
2026 年 Rust 适合 Web 开发吗?
是的,但要有现实的期望。生态系统比以往任何时候都更成熟,但它从一开始就比 Python 或 JavaScript 要求更多。如果你的项目需要性能、内存安全性或长期可靠性,那么投入是值得的。如果你需要在简单的 CRUD 应用上快速推进,目前在别处可能更愉快。
异步 Rust 的主要问题是什么?
回溯变得难以阅读,因为运行时在执行权 yield 时会重置它们。稳定版 Rust 中仍然没有 async drop。理解任务与线程以及如何处理 pinning 在一个本就苛刻的模型上增加了真正的复杂性。性能很棒,但人体工程学正在追赶。
为什么 Rust Web 项目编译时间这么慢?
几件事会相互叠加:泛型的单态化、庞大的依赖树、编译时的宏展开以及异步代码的复杂性。更快的链接器和替代编译器后端等工具有帮助,但没有一个能完全解决它。
cot.rs 是什么?
我们正在构建的一个开箱即用的 Rust Web 框架,旨在让启动一个 Web 项目感觉更像一个整体框架而不是组装工具箱。它包括声明式迁移、可读的查询宏、自动生成的 OpenAPI 文档、管理面板,以及对全栈更集成的思路。你可以在 cot.rs 找到它。
我应该使用哪个 Rust Web 框架?
对于 API,Axum 和 Actix-web 是最广泛使用的。对于更完整的起点、配置更少的情况,Loco.rs 和 cot.rs 值得探索。目前还没有一个单一的主导选择,这也是启动一个新 Rust Web 项目比应有的更困难的部分原因。