Rust in Everything
基于 topcoat 0.10.0

怎么读这一卷

主线位置:三条主线的总览。本页把请求、发现、交互三条主线各画一遍,标出每一步落在哪个文件、哪一章。
flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| u1[01 全景]
  u1 --> u2["02 模块路由与发现"]
  u2 --> u3["04 view、05 运行时"]
  u3 --> u4["07 安全默认值、11 常见坑"]
  q -->|读懂实现| all[00 到 13 按顺序]

锁定版本

项值
cratetopcoat 0.10.0,以及工作区里一起发布的 31 个子 crate(版本号相同)
tagv0.10.0
commit3e7bb43(2026-10-03,chore: release v0.10.0 (#451))
edition / MSRV2024 / 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 没有计时器,读请求头的超时不生效
#518module_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_untilcrates/topcoat/src/serve.rs从连接到分发
accept 循环、三个 watch 通道的关停crates/topcoat-router/src/service.rs同上
catch_unwind、rewrite 循环、dispatchcrates/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-view03 上下文、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 属性。

阅读路线

摘自 topcoat 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的示例用 2 空格缩进。每个示例上方写出依赖。

前置知识

这一卷默认你会写 async Rust,用过某个 Rust Web 框架。topcoat 直接站在 hyper 和 tokio 上,下面这些在别的卷里讲过,这里只链过去:

概念在哪里讲过本卷用到它的地方
hyper 的 Service(call(&self))Service 与 BodyRouterService 实现的是 hyper 的 Service,不是 tower 的
auto::Builder、serve_connection_with_upgradesauto:一个端口两个协议每个连接一个 auto::Builder
优雅关停优雅停机internal_serve 自己用三个 watch 通道实现,没用 hyper-util 的 GracefulShutdown
watch 通道oneshot、watch、broadcast、Notify关停信号「靠关闭传递」
tower 的 Service 与 poll_readyService traitTowerLayer、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 viewview! 展开成什么;组件、并发渲染与流式
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,想读懂实现按顺序读。

💬 关联讨论

发起讨论
加载中...