Rust in Everything
基于 axum 0.8.9

源码地图

主线位置:不在主线上。本页说明两条主线经过的代码在哪个 crate、哪个目录,以及要打开哪个 feature 才能编译到它。

一个 axum 程序依赖的几个 crate(实线是 crate 之间的依赖,虚线是「通常由你自己加进来」):

flowchart TB
  app[你的程序] --> axum[axum]
  app -.-> thttp[tower-http]
  app -.-> macros["axum-macros:macros"]
  axum --> core[axum-core]
  axum --> matchit[matchit]
  axum --> tower[tower]
  axum --> hyper["hyper:http1 或 http2"]
  axum --> hutil["hyper-util:tokio"]
  core --> http[http 与 http-body]
  core --> tsvc[tower-service 与 tower-layer]
  thttp --> tsvc

几个 crate 各管什么

crate版本管什么src/ 行数本卷
axum0.8.9Router、MethodRouter、Handler、内置提取器(Path、Query、Json、Form、State……)、from_fn 等中间件工具、axum::serve19 424全卷
axum-core0.5.6两个提取器 trait FromRequestParts / FromRequest、IntoResponse / IntoResponseParts、Body、DefaultBodyLimit、FromRef3 12902、03
axum-macros0.5.1#[debug_handler]、#[debug_middleware],以及 FromRequest、FromRequestParts、FromRef、TypedPath 的派生宏3 688只讲 debug_handler 和 FromRequest 派生生成了什么
axum-extra0.12.6不够通用、没放进 axum 的东西:cookie、typed header、更多提取器和响应类型7 933不讲
tower-http0.7.1和框架无关的 HTTP 中间件:静态文件、压缩、超时、body 大小限制、CORS、Trace、CatchPanic 等31 61906,只讲最常用的几个

行数是各 tag 下 src/ 里 .rs 文件的总行数,含测试和注释,只用来估分量。axum 的 19 424 行里有 2 447 行是 routing/tests/;另外 src/docs/ 下还有 3 012 行 markdown,不算在内。

axum 自己不实现 HTTP,也不实现路由树:协议在 hyper,路径匹配在 matchit 0.8.4(Cargo.toml 里写死了 matchit = "=0.8.4",连补丁版本都不放开)。tower-http 不依赖 axum,它的 crate 文档说中间件只依赖 http 和 http-body 两个 crate,所以「compatible with any library or framework that also uses those crates」。

axum-core 为什么单独成 crate

axum-core 的 crate 文档只有一句话的定位:

Libraries authors that want to provide [`FromRequest`] or [`IntoResponse`] implementations
should depend on the [`axum-core`] crate, instead of `axum` if possible.

axum 的 crate 文档「Building integrations for axum」一节说得更具体:axum-core「contains core types and traits and is less likely to receive breaking changes」。

写一个提取器或者一个响应类型,只需要这几个 trait,不需要路由、不需要服务器。依赖 axum-core 而不是 axum,好处有两个:

  • 依赖少。axum-core 0.5.6 的必选依赖只有 bytes、futures-core、http、http-body、http-body-util、mime、pin-project-lite、sync_wrapper、tower-layer、tower-service 十个,没有 hyper、没有 tokio、没有 matchit。
  • 少跟着升级。axum 的路由、服务器改了要发破坏性版本,只要 trait 没变,axum-core 不用跟着升大版本,库也就不用跟着发版。axum-core 的 Cargo.toml 还用 cargo_check_external_types 列出公开 API 里允许出现的外部类型(bytes、http、http_body、futures_core、tower_layer),防止不稳定的依赖漏进公开 API。

axum 把这些 trait 原样重新导出(axum::extract::FromRequest 就是 axum_core::extract::FromRequest),用 axum 写应用的人感觉不到这层拆分。

📝NOTE
冷知识:axum-core 是 0.4 才拆出来的

axum 0.3 之前,FromRequest 和 IntoResponse 都定义在 axum 里。issue #561(「Move FromRequest and IntoResponse into their own crate?」)指出,给 axum 做集成的库(maud、tower-cookies 等)大多只用这两个 trait,「These traits are somewhat more stable than the rest of axum」,可是 axum 一升版本,这些库就得跟着升,「to pull in changes that are essentially irrelevant, such as routing」。提议的做法是「Basically the same pattern as futures-core and tower-service」,并且对放进去的东西「be quite conservative」:「Ideally only what has to be moved should be moved.」

#564 照此拆出 axum-core,随 axum 0.4.0(2021-12-02)和 axum-core 0.1.0 发布。axum 的 CHANGELOG 写的是:「This crate is intended for library authors to depend on, rather than axum itself, if possible. axum-core has a smaller API and will thus receive fewer breaking changes.」代价也写在 PR 里:一些宏要在两个 crate 里各写一份,hyper::body::to_bytes 也被复制了一份到 axum-core,「Luckily it had no dependencies on hyper itself.」这就是今天 axum-core 不依赖 hyper 的由来。

axum 的 feature

Cargo.toml 第 14 行起:

[features]
default = [
    "form",
    "http1",
    "json",
    "matched-path",
    "original-uri",
    "query",
    "tokio",
    "tower-log",
    "tracing",
]
form = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
http1 = ["dep:hyper", "hyper?/http1", "hyper-util?/http1"]
http2 = ["dep:hyper", "hyper?/http2", "hyper-util?/http2"]
json = ["dep:serde_json", "dep:serde_path_to_error"]
macros = ["dep:axum-macros"]
matched-path = []
multipart = ["dep:multer"]
original-uri = []
query = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
tokio = ["dep:hyper-util", "dep:tokio", "tokio/net", "tokio/rt", "tower/make", "tokio/macros"]
tower-log = ["tower/log"]
tracing = ["dep:tracing", "axum-core/tracing"]
ws = ["dep:hyper", "tokio", "dep:tokio-tungstenite", "dep:sha1", "dep:base64"]

默认开 9 个,手动开的 4 个是 http2、macros、multipart、ws。每个 feature 管到哪里:

feature默认打开了什么本卷
http1 / http2前者hyper 和 hyper-util 对应的协议;axum::serve 要 tokio 加两者之一才编译hyper 卷
tokio是axum::serve、extract::connect_info、SSE 的 keep_alive01、02、03
json是Json 提取器和响应02、03
form / query是Form、Query 提取器02
matched-path是路由时把匹配到的路由模板存进 extensions,MatchedPath 提取器02、04
original-uri是路由前把原始 URI 存进 extensions,OriginalUri 提取器;嵌套的服务里 Uri 会被去掉前缀,原件从这里取02、04
tracing是内置提取器失败时用 tracing 记一条日志02
tower-log是只是打开 tower 的 log feature—
macros否重新导出 axum-macros 的 debug_handler、debug_middleware10
multipart否Multipart 提取器(底下是 multer)02
ws否extract::ws,WebSocket 升级07

matched-path 和 original-uri 是空 feature,不拉任何依赖,只是在 PathRouter::call_with_state 里打开两段 #[cfg] 代码:每个请求多往 extensions 里插一两个值。

crate 文档里也有一张 feature 表,但 tokio 那一行写的「Enables tokio as a dependency and axum::serve, SSE and extract::connect_info types」已经过时:0.8.5 的 CHANGELOG 记着「The sse module and Sse type no longer depend on the tokio feature」(#3154)。在 0.8.9 里 response/mod.rs 的 pub mod sse; 不挂任何 feature,只有 Sse::keep_alive 和它用的 KeepAliveStream 要 tokio(要用 tokio 的定时器)。

把默认 feature 全关掉,axum 就不依赖 hyper 和 tokio 了。cargo tree -e normal --depth 1 的直接依赖(M2 Max、rustc 1.99.0):

依赖写法axum 的直接依赖
axum = { version = "=0.8.9", default-features = false }17 个:axum-core、bytes、futures-util、http、http-body、http-body-util、itoa、matchit、memchr、mime、percent-encoding、pin-project-lite、serde_core、sync_wrapper、tower、tower-layer、tower-service
axum = "=0.8.9"25 个:上面 17 个,加 form_urlencoded、hyper、hyper-util、serde_json、serde_path_to_error、serde_urlencoded、tokio、tracing

不依赖 hyper 和 tokio 的 axum 仍然有完整的 Router、Handler、提取器,只是没有 axum::serve:Router<()> 本身就是一个 tower Service,可以用 oneshot 直接调用,也可以交给任何接受 tower 服务的服务器。下一部分的「一个请求穿过 axum」就是这样跑的。不过 crate 文档的「Compatibility」一节也说得很清楚:「axum is designed to work with tokio and hyper. Runtime and transport layer independence is not a goal, at least for the time being.」

src/ 的目录

下表的行数是 tag axum-v0.8.9 里各目录 .rs 文件的总行数,含测试和注释:

路径行数管什么本卷
routing/6 486Router、PathRouter(包着 matchit)、MethodRouter、Route、nest 用的 StripPrefix;其中 tests/ 2 447 行04
extract/5 804内置提取器:Path(自带一个 serde Deserializer)、Query、State、MatchedPath、ConnectInfo、ws、multipart,以及各种 rejection02、07
middleware/1 715from_fn、from_extractor、map_request、map_response05
response/1 229Redirect、Html、NoContent、SSE;IntoResponse 本身在 axum-core03
serve/895axum::serve、Listener trait、with_graceful_shutdown01(细节在 hyper 卷)
handler/702Handler trait 和它的宏实现、HandlerService、Handler::layer02
根目录的文件1 883lib.rs(535 行,几乎全是文档)、json.rs、form.rs、extension.rs、boxed.rs(BoxedIntoRoute)等02、04
test_helpers/394只在测试和 __private feature 下编译:起一个真实服务器、用 reqwest 发请求的 TestClient08
error_handling/262HandleError、HandleErrorLayer:把返回错误的 tower 服务变成 Infallible05
body/54重新导出 axum-core 的 Body,加 to_bytes02

routing/ 和 extract/ 占了一半以上,这也是本卷篇幅的分布:02 讲提取器,04 讲路由。

src/docs/:一半的 API 文档在 markdown 里

src/docs/ 下有 21 个 .md 文件、3 012 行。它们不是给人直接读的,而是用 #[doc = include_str!(...)] 拼进 API 文档:整个 src/ 里有 23 处 include_str!。比如 Router::route 的文档:

    #[doc = include_str!("../docs/routing/route.md")]
    #[track_caller]
    pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {

handler/mod.rs 的模块文档也是两段 markdown 拼起来的,第二段 debugging_handler_type_errors.md 就是 docs.rs 上「Debugging handler type errors」那一节。读源码时看到一个方法上面只有一行 include_str!,文档在 src/docs/ 里对应的文件。这些 markdown 里的代码块和普通文档注释一样会被 cargo test --doc 编译运行,所以它们和代码是同步的。

axum-macros:只在 debug 下起作用

#[debug_handler] 的文档第一句是「Generates better error messages when applied to handler functions.」它不改变 handler 的行为,只是在函数旁边生成一串检查代码,让「参数不是提取器」「future 不是 Send」这类错误报在具体的参数上,而不是报成一句 Handler<_, _> 不满足。它的实现第一行就是:

pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
    #[cfg(not(debug_assertions))]
    return input;

release 构建时原样返回输入,什么也不生成。对比报错的例子放在常见坑。

MSRV

rust-version = "1.80",在 workspace 级别统一设置,axum、axum-core、axum-macros 都继承它;edition 2021。workspace 还对所有成员打开了 unsafe_code = "forbid",README 的「Safety」一节说的「This crate uses #![forbid(unsafe_code)] to ensure everything is implemented in 100% safe Rust」就是这一行。

小结

  • axum 管路由、handler、提取器和 serve;axum-core 只放提取器和响应的 trait,给库作者依赖;axum-macros 是可选的诊断与派生宏;axum-extra 不进本卷;tower-http 是与框架无关的中间件集合。
  • axum-core 在 axum 0.4.0 拆出(#564),理由是这些 trait 比 axum 其余部分稳定,集成库不该因为路由改动而跟着升级;它不依赖 hyper 和 tokio。
  • 默认开 form、http1、json、matched-path、original-uri、query、tokio、tower-log、tracing;http2、macros、multipart、ws 要手动开。全关时 axum 不依赖 hyper 和 tokio,但没有 axum::serve。
  • crate 文档的 feature 表里「tokio 打开 SSE」已过时:0.8.5 起只有 Sse::keep_alive 需要 tokio。
  • routing/ 和 extract/ 占了 src/ 的一半以上;src/docs/ 的 21 个 markdown 通过 include_str! 拼成 API 文档。
  • #[debug_handler] 在 release 下不生成任何代码。MSRV 1.80,全 workspace 禁用 unsafe。

💬 关联讨论

发起讨论
加载中...