怎么读这一卷
主线位置:三条主线的总览。本页把请求、发现、交互三条主线各画一遍,标出每一步落在哪个文件、哪一章。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|会用| u1[01 全景]
u1 --> u2["02 模块路由与发现"]
u2 --> u3["04 view、05 运行时"]
u3 --> u4["07 安全默认值、11 常见坑"]
q -->|读懂实现| all[00 到 13 按顺序]
锁定版本
| 项 | 值 |
|---|---|
| crate | topcoat 0.10.0,以及工作区里一起发布的 31 个子 crate(版本号相同) |
| tag | v0.10.0 |
| commit | 3e7bb43(2026-10-03,chore: release v0.10.0 (#451)) |
| edition / MSRV | 2024 / 1.98 |
| 实验用的依赖 | tokio 1.54.1、hyper 1.12.0、hyper-util 0.1.21、matchit 0.9.2 |
源码链接都用 tag,形如 https://github.com/tokio-rs/topcoat/blob/v0.10.0/crates/topcoat-router/src/router.rs#L91。想在本地对照:
git clone https://github.com/tokio-rs/topcoat
cd topcoat
git checkout v0.10.0
这个项目很年轻:第一个提交在 2026-03-26,仓库里最早的 tag 是 2026-07-22 的 v0.4.0,到 v0.10.0 的两个多月里换了七个次版本号,按 0.x 的惯例每一个都可以带破坏性改动。README 开头用粗体写着「Early-stage and experimental. Expect breaking changes.」,浏览器运行时的文档更直接写了「highly experimental」。所以本卷每一处行为都只对 0.10.0 负责。
写作时上游 main 已经比 tag 多了 28 个提交(到 2026-10-14)。其中改到本卷内容的有这几个,读 main 会看到和本卷不一样的代码:
| 提交 | 改了什么 | 本卷在哪里讲 0.10.0 的样子 |
|---|---|---|
| #497 | 给 hyper 的 http1、http2 连接配上 TokioTimer | 从连接到分发:0.10.0 没有计时器,读请求头的超时不生效 |
| #518 | module_router! 只收自己根模块之下的 handler | 模块路由与发现:0.10.0 收整个二进制里的,根之外的直接 panic |
| #501 | 新增 topcoat new 命令 | 0.10.0 的 CLI 只有 dev、fmt、asset、ui 四个子命令,README 的 Roadmap 里也还列着 topcoat new |
| #514、#515、#513 等 | ? 作用于 Result<T, &Error>、大视图的 Send 检查、表单控件在重渲染时保留编辑内容 | 03、04、05 章 |
三条主线
每一页开头都会标出本页在哪条主线上。下面三张图里的每个节点,都对应一个函数或一个宏。
请求主线(服务端 → HTML):从 topcoat::start 到响应体的最后一个字节。
flowchart TB start["topcoat::start:读 HOST、PORT"] --> loop["internal_serve:accept 循环"] loop --> task["每个连接一个任务:auto::Builder"] task --> svc["RouterService::call:插入 RemoteAddr"] svc --> hw["Router::handle_with:catch_unwind"] hw --> hi["handle_inner:新 Cx、rewrite 循环"] hi --> disp["dispatch:matchit、按方法挑路由"] disp --> origin["OriginLayer:永远最外层"] origin --> next["Next::run:layer 链"] next --> page["page 的 render:BoxView"] page --> stream["content/view.rs:首屏 + 追加的 template"] stream --> comp["Compression:所有 layer 之外"]
| 一步 | 源码 | 本卷 |
|---|---|---|
start / serve / serve_until | crates/topcoat/src/serve.rs | 从连接到分发 |
accept 循环、三个 watch 通道的关停 | crates/topcoat-router/src/service.rs | 同上 |
catch_unwind、rewrite 循环、dispatch | crates/topcoat-router/src/router.rs | 同上 |
| 端点、路由、layer 链 | endpoint.rs、route.rs、layer.rs、builder.rs | 路由表与 layer 链 |
| 视图变成分块响应 | crates/topcoat-router/src/content/view.rs | 一个请求从监听到 HTML |
| 错误、重定向变成响应 | crates/topcoat-router/src/error.rs | 请求、响应与错误 |
Cx、View、suspense 本身 | topcoat-core、topcoat-view | 03 上下文、04 view |
发现主线(启动时,路由从哪来):没有 build.rs,不扫文件系统,mod 声明就是路由表。
flowchart TB attr["#[page] 展开"] --> sub["inventory::submit:登记 dyn ModulePage"] attr --> mp["module_path:记下所在模块"] mr["module_router! 展开"] --> mrb["ModuleRouterBuilder::new(module_path)"] mrb --> iter["discover:遍历 inventory"] sub --> iter mp --> iter iter --> seg["按 :: 切段:_ 开头是组,其余 kebab-case"] seg --> rb["RouterBuilder:普通的 page、route、layer"] rb --> build["build:matchit 路由表"]
这条线全在 模块路由与发现 一页里。asset! 走的是另一种「发现」:在编译出的二进制里留一段字节标记,由 CLI 扫出来,放在 asset! 与 bundler。
交互主线(浏览器 → 服务端 → 局部 HTML):$(...) 表达式、signal、shard 和 procedure。这条线在 05 浏览器运行时,00 到 02 部分只会在 一个请求从监听到 HTML 看到它留在 HTML 里的痕迹:一段 <!--::topcoat::signal(...)--> 注释,和一个 data-topcoat-bind:hidden 属性。
阅读路线
- 只想会用:读 01 全景、02 里的 模块路由与发现,然后 04 view、05 浏览器运行时、07 安全默认值、11 常见坑。读完能按模块组织页面、写组件和交互,知道哪些默认值会拦住你的请求。
- 想读懂实现:按顺序读。02 router 是本卷重点,请求主线和发现主线都落在这里,后面每章都会链回来;05 是 topcoat 和其他框架最不一样的地方。
摘自 topcoat 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的示例用 2 空格缩进。每个示例上方写出依赖。
前置知识
这一卷默认你会写 async Rust,用过某个 Rust Web 框架。topcoat 直接站在 hyper 和 tokio 上,下面这些在别的卷里讲过,这里只链过去:
| 概念 | 在哪里讲过 | 本卷用到它的地方 |
|---|---|---|
hyper 的 Service(call(&self)) | Service 与 Body | RouterService 实现的是 hyper 的 Service,不是 tower 的 |
auto::Builder、serve_connection_with_upgrades | auto:一个端口两个协议 | 每个连接一个 auto::Builder |
| 优雅关停 | 优雅停机 | internal_serve 自己用三个 watch 通道实现,没用 hyper-util 的 GracefulShutdown |
watch 通道 | oneshot、watch、broadcast、Notify | 关停信号「靠关闭传递」 |
tower 的 Service 与 poll_ready | Service trait | TowerLayer、TowerRoute、TowerService |
| matchit 的路径语法 | matchit 与路径语法 | 路由表;axum 0.8.9 用 matchit 0.8.4,topcoat 用 0.9 |
| Dioxus fullstack 的水合与流式 | 水合、流式渲染 | 01 的定位对比、09 章 |
目录
| 部分 | 内容 |
|---|---|
| 00 导读 | 本部分;源码地图 |
| 01 全景 | topcoat 解决什么问题;一个请求从监听到 HTML |
| 02 router | 本卷重点:从连接到分发、路由表与 layer 链、模块路由与发现、请求响应与错误、tower 互操作与可信代理 |
| 03 上下文 | Cx、AppContext 与 #[memoize] |
| 04 view | view! 展开成什么;组件、并发渲染与流式 |
| 05 浏览器运行时 | $(...) 表达式;signal、shard 与 procedure |
| 06 资产与工具链 | asset! 与 bundler;tailwind、font、icon 与 CLI |
| 07 安全默认值 | Origin 检查、cookie 与 session |
| 08 第三方交互集成 | htmx、Datastar、Alpine AJAX |
| 09 与本站方案对比 | topcoat 与 Dioxus fullstack |
| 10 设计取舍 | 把各章的取舍汇总到一起 |
| 11 常见坑 | 用报错原文做标题,讲它在源码里怎么产生 |
| 12 在本站中 | 把本站一页用 topcoat 写会怎样 |
| 13 动手 | 用 hyper 和 matchit 写一个迷你版的思路 |
小结
- 全卷锁定 tag
v0.10.0(3e7bb43)。上游自称早期、实验性;tag 之后的 #497(hyper 计时器)和 #518(模块发现范围)改到了 02 章讲的代码,本卷写的是改之前的样子。 - 请求主线:
start→ accept 循环 →RouterService→catch_unwind→ 新Cx与 rewrite 循环 → matchit 分发 → Origin 检查 → layer 链 → 流式视图 → 压缩。 - 发现主线:属性宏用 inventory 在链接期登记、记下
module_path!(),module_router!()在启动时把模块路径切成 URL。 - 交互主线在 05 章;只想会用读 01、02 模块路由、04、05、07、11,想读懂实现按顺序读。