源码地图
主线位置:不在主线上。本页说明三条主线经过的代码在哪个 crate、哪个目录,以及要打开哪个 feature 才能编译到它。
一个用默认 feature 的 topcoat 程序依赖的 crate(实线是 crate 之间的依赖,虚线是只在某个 feature 下才有):
flowchart TB app[你的程序] --> facade["topcoat:门面"] facade --> router[topcoat-router] facade --> view[topcoat-view] facade --> runtime[topcoat-runtime] facade --> web["cookie、session、asset、font、icon"] facade -.-> third["htmx、datastar、alpine-ajax、tailwind、mail"] runtime --> router web --> router router --> view view --> core[topcoat-core] router --> hyper["hyper、hyper-util:serve"] router --> matchit["matchit 0.9"] router -.-> towerhttp["tower、tower-http:compression"]
四层 17 个 crate
| 层 | crate | 管什么 | src 行数 | 本卷 |
|---|---|---|---|---|
| 核心 | topcoat-core | Cx 与上下文、#[memoize]、Error、组件身份 Identity | 4.8k + grammar 6.7k | 03 |
topcoat-view | view!、View trait、组件、suspense、live! | 8.7k + grammar 9.1k | 04 | |
topcoat-router | 服务器、路由表、layer、模块路由、请求与响应、错误 | 23.1k + grammar 2.4k | 02 | |
topcoat-runtime | $(...)、signal、shard、procedure,以及浏览器端脚本 | 4.2k + grammar 3.0k + TypeScript 4.3k | 05 | |
| Web 能力 | topcoat-cookie | cookie jar,签名、加密、前缀 cookie | 2.1k | 07 |
topcoat-session | 会话:随机 token、只存哈希、存储自己实现 | 0.6k | 07 | |
topcoat-asset | asset!、资产包的加载与服务、bundler | 2.8k | 06 | |
topcoat-font | 字体声明,可选 Fontsource | 2.6k + grammar 3.9k | 06 | |
topcoat-icon | 图标,可选 Iconify | 0.8k + grammar 0.7k | 06 | |
topcoat-mail | mail!,SMTP、文件、内存三种传输 | 2.6k + grammar 0.4k | 08 | |
| 第三方集成 | topcoat-tailwind | 在 build.rs 里下载 Tailwind 独立 CLI | 0.7k | 06 |
topcoat-htmx | htmx 的请求头读取、响应头构造 | 1.0k | 08 | |
topcoat-datastar | Datastar 的 signals 读取与 SSE 补丁 | 1.2k | 08 | |
topcoat-alpine-ajax | Alpine AJAX 的请求头 | 0.2k | 08 | |
| 工具 | topcoat-cli | topcoat dev、fmt、asset、ui | 3.6k | 06 |
topcoat-ui | 读组件注册表、管理已安装组件(按源码哈希判断更新),给 topcoat ui add 用;组件源码本身在 topcoat-ui-registry(3.9k) | 1.7k | 06 | |
| 门面 | topcoat | 按 feature 重新导出上面的 crate,加 start / serve | 0.7k | 本页 |
行数是 tag v0.10.0 下各 crate src/ 里 .rs 文件的总行数,含文件内的 #[cfg(test)] 测试和文档注释,只用来估分量。整个仓库 768 个 .rs 文件;topcoat-router 一家占了全部 crate 源码的四分之一,其中第一个 #[cfg(test)] 之前的部分约 1.3 万行,测试函数 606 个。另外还有 topcoat-runtime-coherence(2.2k,不发布,05 章讲它怎么用 V8 对拍 Rust 和 JavaScript 的结果)、examples/ 下 26 个示例和 benchmarks/。benchmarks/ 里放着两个用来对比的项目:axum-maud 和 leptos,上游自己拿来比较的就是这两类方案。
依赖方向很清楚:topcoat-core 不依赖任何 topcoat crate;topcoat-view 只依赖 core;topcoat-router 依赖 core 和 view;Web 能力那一层的 crate 大多依赖 router,各自通过一个扩展 trait 往 RouterBuilder 上加方法(RouterBuilderAssetExt::assets、RouterBuilderRuntimeExt::runtime 等)。
一个带宏的 crate 拆成三个
带宏的 crate 有七个:core、view、router、runtime、font、icon、mail。每个都拆成三个 crate:
| crate | 内容 | 例子 |
|---|---|---|
X | 运行时类型和函数,宏展开后的代码调用它们 | topcoat-router:Router、ModulePage、Path |
X-grammar | 普通库:用 syn 写的 Parse 和 ToTokens,也就是宏的语法树和代码生成 | topcoat-router-grammar:page.rs 里的 Page |
X-macro | proc-macro = true,每个入口只做「解析、生成、出错就 to_compile_error」 | topcoat-router-macro:79 行 |
七个 macro crate 加起来不到 300 行,最短的 core 只有 13 行,最长的 router 79 行。router 的 #[page] 入口就是这样:
#[proc_macro_attribute]
pub fn page(attr: TokenStream, item: TokenStream) -> TokenStream {
match topcoat_router_grammar::page::Page::parse(attr.into(), item.into()) {
Ok(value) => quote! { #value }.into(),
Err(error) => error.to_compile_error().into(),
}
}
拆出 grammar 的好处是它可以被别的 crate 当普通库依赖。proc-macro crate 只能导出宏,不能导出类型,放在里面的语法树别人用不了。拆出来之后:
topcoat fmt复用同一套语法树。仓库的AGENTS.md写着「Macro-body formatters live behind each grammar crate'sprettyfeature and are used bytopcoat fmt」。topcoat-cli依赖的是 core、view、font、mail 四个 grammar 的prettyfeature:格式化器和宏用同一个Parse解析view!的内容,不会出现「宏能编译、格式化器不认识」的语法。router、runtime、icon 的 grammar 没有prettyfeature;topcoat fmt认的宏只有view!、attributes!、class!、几个字体宏和mail!(fmt.rs第 44 行起)。- 宏的解析和代码生成能写成普通单元测试。七个 grammar 的测试都借
topcoat-core-grammar的testingfeature,直接对TokenStream断言,不用经过编译器的 proc-macro 边界。
生成代码里的路径也有讲究。paths::resolve 在宏展开时用 proc-macro-crate 读调用方的 Cargo.toml:直接依赖了 topcoat-view 的(比如一个组件库)就生成 ::topcoat_view::...,只依赖门面的应用生成 ::topcoat::view::...,依赖改了名也照用新名字。门面 crate 自己在 lib.rs 第 4 行写了 extern crate self as topcoat;,让这些路径在它自己的代码里也能解析。下一部分会看到一个 #[page] 展开后满屏的 ::topcoat::router::...,就是这么来的。
门面 crate 的 feature
crates/topcoat/Cargo.toml 第 15 行起(原文件用 tab 缩进):
[features]
default = [
"asset",
"compression",
"cookie",
"font",
"icon",
"router",
"runtime",
"serve",
"session",
"view",
"discover",
]
默认开 11 个。full 在这 11 个之外再加 16 个:alpine-ajax、anyhow、datastar、font-fontsource、fs、htmx、icon-iconify、mail、mail-smtp、multipart、sitemap、sse、tailwind、tower、ui、websocket。大部分 feature 就是打开一个子 crate,几个要单独说:
| feature | 默认 | 打开了什么 | 本卷 |
|---|---|---|---|
router | 是 | topcoat-router 和它的宏;同时打开 asset、cookie、font、mail、runtime、session 各自的 router 子 feature,那些 RouterBuilder*Ext 扩展 trait 就在这些子 feature 里 | 02 |
serve | 是 | start / serve / serve_until,带上 hyper、hyper-util、tokio;还打开 tokio-tungstenite,给 topcoat dev 报「已就绪」用 | 02 |
discover | 是 | inventory;module_router! 和 .discover() 都要它 | 02 |
compression | 是 | 隐含 router;拉进 tower 和 tower-http,只为了用 tower-http 的 gzip / br 压缩 | 02 |
runtime | 是 | $(...)、signal、shard、procedure | 05 |
tower | 否 | TowerRoute、TowerLayer、TowerService | 02 |
fs | 否 | 静态目录路由 | 02 |
sse、websocket、multipart、sitemap | 否 | router 里 content/ 下对应的模块;websocket 隐含 serve | 02 |
datastar | 否 | 隐含 sse,因为 Datastar 的补丁走 SSE | 08 |
tailwind、font-fontsource、icon-iconify | 否 | 都要在构建时联网下载 | 06 |
router 那一行值得多看一眼:
router = [
"dep:topcoat-router",
"dep:topcoat-router-macro",
"view",
"topcoat-asset?/router",
"topcoat-cookie?/router",
"topcoat-font?/router",
"topcoat-mail?/router",
"topcoat-runtime?/router",
"topcoat-session?/router",
]
?/ 是「如果这个依赖已经被别的 feature 打开,就顺带打开它的 router」,不会因此把 cookie、session 拉进来。所以「只要 view,不要路由」是可以的:关掉默认 feature,只开 view,就能把 view! 当成一个独立的 HTML 模板库用。仓库里的 examples/routerless 就是这样依赖的:default-features = false, features = ["view"],自己建一个 Cx 把组件渲染成字符串。
依赖规模,用 cargo tree -e normal -p topcoat 数去重后的 crate 个数(含 topcoat 自己):
| 依赖写法 | crate 数 |
|---|---|
topcoat = "=0.10.0" | 167 |
topcoat = { version = "=0.10.0", features = ["full"] } | 230 |
两种写法里都没有 axum,但都有 hyper、hyper-util、matchit、tower、tower-http(后两个来自默认开的 compression)。
工作区的约定
根 Cargo.toml 里有三条会影响读源码的设置:
rust-version = "1.98",edition 2024。源码里能看到 let 链(if let ... && ...)、返回位置impl Trait的use<>精确捕获这类新语法。unsafe_code = "deny",tag 里没有一处allow(unsafe_code)。用到unsafe的地方都在依赖里:inventory 的链接期注册(下一部分展开一个#[page]就能看到)。- clippy 的
mod_module_files = "deny":不许用mod.rs,一律写成module.rs加同名目录module/。读 router 时会看到module.rs只有 22 行、真正的代码在module/下,就是这个原因。
小结
- 17 个发布的主 crate 分核心(core、view、router、runtime)、Web 能力(cookie、session、asset、font、icon、mail)、第三方集成(tailwind、htmx、datastar、alpine-ajax)、工具(cli、ui)四层,外加门面
topcoat;router 最大,2.3 万行。 - 带宏的七个 crate 都拆成运行时、grammar、macro 三件;macro crate 只是一层转发,grammar 是普通库,
topcoat fmt通过prettyfeature 复用其中四个。 - 生成代码的路径在展开时读调用方的
Cargo.toml决定,直接依赖子 crate 和只依赖门面都能编译。 - 默认开 11 个 feature,
full再加 16 个;router用?/顺带打开各子 crate 的路由扩展;默认依赖树 167 个 crate,没有 axum。