Rust in Everything
基于 topcoat 0.10.0

源码地图

主线位置:不在主线上。本页说明三条主线经过的代码在哪个 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-coreCx 与上下文、#[memoize]、Error、组件身份 Identity4.8k + grammar 6.7k03
topcoat-viewview!、View trait、组件、suspense、live!8.7k + grammar 9.1k04
topcoat-router服务器、路由表、layer、模块路由、请求与响应、错误23.1k + grammar 2.4k02
topcoat-runtime$(...)、signal、shard、procedure,以及浏览器端脚本4.2k + grammar 3.0k + TypeScript 4.3k05
Web 能力topcoat-cookiecookie jar,签名、加密、前缀 cookie2.1k07
topcoat-session会话:随机 token、只存哈希、存储自己实现0.6k07
topcoat-assetasset!、资产包的加载与服务、bundler2.8k06
topcoat-font字体声明,可选 Fontsource2.6k + grammar 3.9k06
topcoat-icon图标,可选 Iconify0.8k + grammar 0.7k06
topcoat-mailmail!,SMTP、文件、内存三种传输2.6k + grammar 0.4k08
第三方集成topcoat-tailwind在 build.rs 里下载 Tailwind 独立 CLI0.7k06
topcoat-htmxhtmx 的请求头读取、响应头构造1.0k08
topcoat-datastarDatastar 的 signals 读取与 SSE 补丁1.2k08
topcoat-alpine-ajaxAlpine AJAX 的请求头0.2k08
工具topcoat-clitopcoat dev、fmt、asset、ui3.6k06
topcoat-ui读组件注册表、管理已安装组件(按源码哈希判断更新),给 topcoat ui add 用;组件源码本身在 topcoat-ui-registry(3.9k)1.7k06
门面topcoat按 feature 重新导出上面的 crate,加 start / serve0.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-macroproc-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's pretty feature and are used by topcoat fmt」。topcoat-cli 依赖的是 core、view、font、mail 四个 grammar 的 pretty feature:格式化器和宏用同一个 Parse 解析 view! 的内容,不会出现「宏能编译、格式化器不认识」的语法。router、runtime、icon 的 grammar 没有 pretty feature;topcoat fmt 认的宏只有 view!、attributes!、class!、几个字体宏和 mail!(fmt.rs 第 44 行起)。
  • 宏的解析和代码生成能写成普通单元测试。七个 grammar 的测试都借 topcoat-core-grammar 的 testing feature,直接对 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、procedure05
tower否TowerRoute、TowerLayer、TowerService02
fs否静态目录路由02
sse、websocket、multipart、sitemap否router 里 content/ 下对应的模块;websocket 隐含 serve02
datastar否隐含 sse,因为 Datastar 的补丁走 SSE08
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 通过 pretty feature 复用其中四个。
  • 生成代码的路径在展开时读调用方的 Cargo.toml 决定,直接依赖子 crate 和只依赖门面都能编译。
  • 默认开 11 个 feature,full 再加 16 个;router 用 ?/ 顺带打开各子 crate 的路由扩展;默认依赖树 167 个 crate,没有 axum。

💬 关联讨论

发起讨论
加载中...