从 Leptos 迁移到 Topcoat:SSR 与响应式写法完整对照指南
【免费下载链接】topcoatA batteries-included framework for building web apps项目地址: https://gitcode.com/GitHub_Trending/top/topcoat
Topcoat是一个功能完备的 Rust 全栈 Web 框架(batteries-included framework),主打"服务端渲染 + 无 wasm、无前端构建步骤的客户端响应式"。如果你正考虑从 Leptos 迁移到 Topcoat,这篇文章用官方基准仓库中同一套商店应用的真实源码(benchmarks/leptos/与benchmarks/topcoat/两边渲染结果完全一致,由 scripts/verify_parity.sh 强制校验),帮你逐点对照 SSR 数据获取与响应式写法的差异,快速完成迁移。
一、迁移前:哪些不变,哪些变了
| 维度 | Leptos | Topcoat |
|---|---|---|
| 模板语法 | view!宏 | view!宏(思路相近,更接近原生 HTML) |
| 组件 | #[component] | #[component](且天然支持async) |
| 服务端取数 | #[server]server functions +Resource+Suspense | 组件本身就是 async,直接查库,无需"服务端函数"这一层 |
| 客户端响应式 | signal + hydration(需编译 wasm) | $(...)表达式 +signal+@/:属性,无 wasm、无客户端构建 |
| 路由 | leptos_router的Router/Routes/Route | 按模块目录自动发现路由(module routing),零配置 |
| 构建链 | cargo-leptos、wasm32 target、hydrate | 一个cargo build --release即可,可选topcoat asset bundle |
一句话概括:Leptos 的"server function + Resource + hydrate"三层,在 Topcoat 里被压平成了"async 组件 + 信号表达式"两层。
二、SSR 对照:服务端渲染与数据获取
2.1 Leptos 写法:server function + Resource + Suspense
Leptos 中页面组件不能直接查数据库,必须先定义#[server]函数,再用Resource拉取、Suspense包裹等待(见 benchmarks/leptos/src/pages/products.rs):
// 取数:server function let data = Resource::new( move || (page, sort, category), // 依赖变化时重新计算 |(page, sort, category)| get_products(page, sort, category), ); view! { <Suspense fallback=|| ()> {move || Suspend::new(async move { data.await.ok().map(products_view) })} </Suspense> }对应的#[server]函数定义在 benchmarks/leptos/src/server_fns.rs 中。也就是说:取数逻辑、UI 组件、异步等待三处代码需要保持类型一致,这是 Leptos SSR 最大的心智负担。
2.2 Topcoat 写法:组件本身就是 async
Topcoat 的页面函数直接async,在函数体内查数据、在view!里渲染,一步到位(见 benchmarks/topcoat/src/app/products.rs):
#[query_params(error = bad_request)] struct ProductsQuery { page: Option<usize>, sort: Option<String>, category: Option<String> } #[page] async fn products(cx: &Cx) -> Result<impl View> { let query = query_params::<ProductsQuery>(cx)?; // 类型安全的 query 参数 let catalog = app_context::<Catalog>(cx); // 应用级共享数据 let page = catalog.page(query.page.unwrap_or(1), sort, category); Ok(view! { /* 直接用 page 渲染 */ }) }两个关键概念替代了 server function 体系:
app_context:按类型共享的长生命周期数据(如商品目录),一次加载、全应用读取,见 crates/topcoat/docs/app_context.md。基准应用的入口写法在 benchmarks/topcoat/src/app.rs:pub fn router() -> Router { topcoat::router::module_router!() .app_context(Catalog::load()) .assets(AssetBundle::load().expect(...)) .build() }#[query_params]:结构体 + 派生,自动从 URL 解析并校验 query 参数,替代手动use_query_map()。
2.3 模板循环:.collect_view()vs 原生控制流
Leptos 在模板里循环需要iter().map(...).collect_view()的函数式写法;Topcoat 的view!支持直接写for和if/else,更贴近 HTML 直觉(对照 benchmarks/leptos/src/components.rs 与 benchmarks/topcoat/src/app/_components.rs):
// Topcoat:直接 for for (title, links) in FOOTER_COLUMNS { <div> <h3>(title)</h3> for (label, href) in links { <li><a href=(href)>(label)</a></li> } </div> } // Leptos:需要 collect_view {COLUMNS.iter().map(|(title, links)| view! { ... }).collect_view()}迁移要点:把每个#[server]fn 的函数体搬进对应页面/组件的 async 函数里;Resource + Suspense + Suspend三件套直接删除;use_query_map()换成#[query_params]结构体。
三、响应式写法对照:signals 与$(...)
3.1 心智模型差异
| Leptos | Topcoat | |
|---|---|---|
| 状态 | signal(|| 0) | signal(cx, \|\| 0)(多一个请求上下文参数) |
| 响应式取值 | 闭包中count.get(),依赖追踪自动重算 | $(count.get()):服务端先算一次生成初始 HTML,同时翻译成 JS 在浏览器里随信号变化即时重跑 |
| 生效方式 | 需要 wasm 编译 + hydration | 无 wasm、无客户端构建,JS 随页面一起下发 |
| 需要服务端的更新 | #[server]函数 + 手动set_resource | #[shard]组件:参数一变,服务端自动重渲染并原地替换 HTML |
Topcoat 的$(...)表达式本质是普通 Rust 代码被"双编译":服务端求值产出首屏 HTML,等价 JS 随页面在浏览器重跑,见 crates/topcoat/docs/runtime.md。
3.2 事件处理与属性绑定:API 几乎一一对应
两边事件处理都是"事件 → 改信号 → 自动更新",迁移成本极低:
// Topcoat:@ 前缀挂事件,: 前缀做双向绑定 <button @click=$(|_e| count.set(count.get() + 1.0))>"+1"</button> <p>"Count: " $(count.get())</p> <input :value=$(name.get()) @input=$(|e: Event| name.set(e.target.value))>- Leptos 的
on:click=move |_| ...→ Topcoat 的@click=$(|_e| ...) - Leptos 的
prop:value=...受控组件 → Topcoat 的:value=$(...)bind 属性 - Leptos 的
on:input=...→@input=$(|e: Event| ...),事件对象提供e.target.value、e.key等字段
3.3 需要服务端参与时的升级路径
Leptos 里凡是跨"客户端 ↔ 服务端"的调用都要写 server function;Topcoat 提供两个更轻的机制(官方示例见 examples/runtime/ 与 crates/topcoat/src/runtime/):
#[shard]:适合"搜索结果随输入刷新"这类需要查库的场景——组件标记为 shard 后,它的$(...)参数一变,Topcoat 就在服务端重渲染并把新 HTML 原地换入;#[procedure]:适合表单提交这类一次性动作——浏览器直接调用服务端异步函数。
四、路由与项目结构:从声明式到目录式
Leptos 需要显式声明路由树(benchmarks/leptos/src/app.rs):
<Router> <Routes fallback=|| "Not found."> <Route path=path!("products/:id") view=ProductDetailPage ssr=SsrMode::Async/> ... </Routes> </Router>Topcoat 则从模块目录自动推导路由表,无需构建步骤(crates/topcoat-router/README.md):
src/ |-- app.rs -> / (根布局 <html>) `-- app/ `-- products/ `-- id.rs -> /products/{post_id}布局用#[layout]标记(对照 benchmarks/topcoat/src/app.rs 的root_layout),Slot<'_>替代 Leptos 的Outlet。基准项目完整目录可参考 benchmarks/topcoat/。
五、迁移清单:四步走 ✅
- 搬结构:按
app.rs+app/目录重新组织路由,删掉leptos_router声明;每个#[server]fn 内联进对应 async 组件。 - 换取数:全局数据改
app_context;query 参数改#[query_params];删除全部Resource/Suspense/hydrate代码(对照 benchmarks/leptos/src/lib.rs 里的hydrate入口,Topcoat 端完全不需要)。 - 改写响应式:
signal加cx参数;模板里.get()读取包进$(...);on:xxx改@xxx=$(),受控 prop 改:value=$(...)。 - 补构建:
cargo-leptos + wasm32 target从工具链中移除,可选跑一次topcoat asset bundle处理静态资源。
六、什么时候值得迁移?
- 想砍掉wasm 编译、hydration、server function 三层样板,只维护一套 async Rust —— Topcoat 的卖点正在于此;
- 页面以服务端渲染为主、交互局部化:
$(...)覆盖纯浏览器交互,#[shard]/#[procedure]兜底服务端交互; - ⚠️ 注意:Topcoat 官方明确标注早期实验阶段,客户端运行时(runtime)支持的语言子集仍有限,升级前建议先读 crates/topcoat/docs/runtime.md 中的表达式词汇表,确认你现有的响应式模式是否被覆盖。
两边逐行可比的源码都在 benchmarks/(含 Next.js 与 Axum+Maud 基线),配合 benchmarks/scripts/bench.sh 还能横向对比服务端渲染性能,是迁移验证时的最佳参照物。
【免费下载链接】topcoatA batteries-included framework for building web apps项目地址: https://gitcode.com/GitHub_Trending/top/topcoat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考