Topcoat 是一个模块化的、开箱即用的 Rust 框架,用于构建全栈响应式 Web 应用。它优先考虑简洁性和生产力。Topcoat 完全采用服务端渲染。通过在服务端渲染 HTML 片段并将“响应式指令”作为元数据添加(类似于 HTMX 的理念),实现响应式。

以下是使用 Topcoat 构建的“Hello world”应用:

#[tokio::main]
async fn main() {
    topcoat::start(Router::builder().discover().build()).await.unwrap();
}

#[page("/")]
async fn home() -> Result {
    view! {
        <!DOCTYPE html>
        <html>
            <head>
                <title>"Hello world"</title>
                topcoat::dev::script()
            </head>
            <body>
                hello(name: "World")
            </body>
        </html>
    }
}

#[component]
async fn hello(name: &str) -> Result {
    view! {
        <h1>"Hello, " (name) "!"</h1>
    }
}

入门指南

动机

在深入技术细节之前,值得讨论一下 Topcoat 背后的动机。如果您看过我的 TokioConf 演讲(那场我预测 Rust 可能成为所有绿地开发的前 3 名语言的演讲),这些都不会让您感到意外。

三年前,如果我告诉您 Rust 是一种构建 Web 应用的优秀语言,您可能会认为我疯了,而且这样做是合理的。毕竟,Web 应用传统上并不是对性能敏感的应用。所以,适合这项工作的工具应该是能让您快速交付的工具。性能是锦上添花。毫不奇怪,最丰富的 Web 应用生态系统存在于 JavaScript、Ruby 和 PHP 等注重生产力的语言中。

然而,AI 已经彻底改变了这一计算方式。AI 消除了学习障碍和生产力差距。现代编码 AI 工具构建某物所需的时间差异,主要取决于可用库的集合,而非编程语言。即使是操作者的具体专业知识也变得不那么重要。我见过从未写过 Rust 的经验丰富的软件工程师使用 AI 工具从第一天起就与 Rust 一起构建。我说的不是氛围编程,而是利用通用工程经验与 AI 工具交互式地合作,在学习语言的同时取得进展。

现在,我认为 Rust 是一种优秀的语言,它具有一些非常出色的特性(速度和可靠性)。然而,这并不意味着如果您已经拥有一个适合您的技术栈,并且不需要 Rust 的性能和可靠性,我认为您应该切换到 Rust(好吧,我确实这么认为,但我也知道这是我对 Rust 的热爱,而非实际建议)。然而,许多组织正在采用 Rust,因为他们需要解决需要高性能和可靠语言的问题。已经采用 Rust 的组织有很多理由在更高级别的应用中继续使用 Rust,不是因为他们需要性能,而是因为他们已经围绕该语言建立了内部基础设施(库、构建系统、流程……),减少组织内编程语言和工具的数量确实能提高生产力。

因此,现在我们需要一个丰富的库生态系统,才能高效地使用 Rust 构建 Web 应用。这就是我一直在构建的东西。我从 Toasty 开始,这是一个 Rust 的 ORM,因为这可能是最困难的部分。Toasty 自 2026 年 4 月以来就已可供使用。路线图的下一步是 Web 框架,这就是 Topcoat 的用武之地。

去年年底,我遇到了 Julien Scholz,并对他在 Rust 中构建优秀 Web 应用框架的品味和热情印象深刻,所以我说服他花时间构建一个:Topcoat

无需 WebAssembly 的响应式

LeptosDioxus 这样的框架,通过将代码编译为 WebAssembly 并在浏览器中运行,非常适合使用 Rust 创建高度交互式的 Web 应用。然而,许多应用并不需要这种级别的交互性。对于这些用例,编译到单独的目标、担心包大小和拆分,以及在客户端/服务端边界序列化数据都会成为负担。Topcoat 采用了一种更简单的方法。所有标记都在服务端渲染,因此组件可以是 async,可以安全地访问数据库或验证用户权限。要添加响应式,它使用宏将完全类型检查的 Rust 表达式子集交叉编译为 JavaScript,让您无需接触 WebAssembly 即可留在 Rust 中。在此示例中,<button> 点击时会显示下方的 <p> 标签:

view! {
    // 声明一个客户端状态变量:
    signal open = false;
    
    <button
        // 将 Rust 闭包配置为此按钮的“点击”处理程序。
        // $(...) 内的代码在浏览器中作为 JavaScript 运行:
        @click=$(|_e| open.set(!open.get()))
    >
        "What is Topcoat?"
    </button>
    
    // `hidden` 属性跟踪 `open` 的值,并在每次按下按钮时更新
    <p :hidden=$(!open.get())>"A fullstack Rust framework."</p>
}

切换逻辑完全在浏览器中运行,无需服务端往返。

Topcoat 还可以在客户端状态更改时在服务端重新渲染 UI 的整个部分,并仅替换页面中必要的部分。在此示例中,当用户在搜索 <input> 字段中输入时,搜索结果会更新:

#[component]
async fn search() -> Result {
    view! {
        signal query = String::new();

        // 将当前文本输入写入 `query` 信号:
        <input @input=$(|e: Event| query.set(e.target.value))>

        // 随着用户输入而更新。
        search_results(query: $(query.get()))
    }
}

// Shards 是一种特殊类型的组件,它从您的路由器公开 API 端点。
#[shard]
async fn search_results(cx: &Cx, query: String) -> Result {
    // 此函数在服务端运行。它可以异步访问数据库。
    view! {
        <ul>
            for product in search_products(cx, &query).await? {
                <li>(product.name)</li>
            }
        </ul>
    }
}

客户端响应式系统仍处于开发的早期阶段,并且有一些限制。我们未来有很多想法来改进这一点。同时,您也可以使用 HTMXAlpine.js 集成。

构建 Web 界面远比渲染 HTML 复杂。Web 应用需要加载自定义字体、样式表、图像等。Topcoat 提供了一个完整的资源管道,使用 asset 宏:

const FERRIS: Asset = asset!("./ferris.png");

view! { <img src=(FERRIS)> }

在构建时,Topcoat CLI 会收集或下载所有资源,并将它们存储在单个目录中。当您的应用运行时,它们会使用内容哈希提供服务,以优化浏览器缓存。

每个优秀的设计都需要自己的字体和图标。FontsourceIconify 为您提供了大量可免费使用的字体和图标库。这些可以轻松地包含在您的 Topcoat 应用中:

// 从 Fontsource 加载“Roboto”Web 字体。
const ROBOTO: Font = fontsource_font!(ROBOTO);

// 创建一个包含“feather”图标集的 Rust 模块。
iconify::include!("feather");

设计一个有凝聚力的设计系统及其所有组件可能具有挑战性。组件库可以让您快速启动,但当您的设计发生变化时,通常会变得不灵活。受 shadcn/ui 启发,Topcoat 的内置组件库基于 Tailwind,并将现成的组件直接复制到您的源代码目录中。这意味着您可以修改任何需要的内容,并真正拥有自己的设计:

#[component]
async fn delete_card() -> Result {
    view! {
        card(
            card_header(
                card_title("Delete workspace")
                card_description("This permanently removes the workspace and all of its data.")
            )
            card_footer(
                attrs: attributes! { class="justify-end" },
                button(variant: ButtonVariant::Ghost, "Cancel")
                button(variant: ButtonVariant::Destructive, "Delete workspace")
            )
        )
    }
}

行为局部性作为指导原则

人类和 AI 都更擅长在代码的小区域内进行推理。Topcoat 从底层架构设计,允许您保持逻辑局部且可组合。例如,我们鼓励让组件自己进行数据获取,而不是将数据作为组件参数传递下去:

#[component]
async fn user_profile(cx: &Cx, user_id: &str) -> Result {
    // 只有此组件知道它需要哪些用户数据。
    let user = load_user(cx, user_id).await?;
    view! {
        <h1>(user.name)</h1>
        ...
    }
}

为了避免多次获取数据,Topcoat 具有内置的请求级记忆化,灵感来自 React 的缓存

#[memoize]
async fn load_user(cx: &Cx, user_id: &str) -> Result<User> {
    // 此数据库调用仅对每个唯一的 `user_id` 执行一次。
    db(cx).load_user_by_id(user_id).await
}

这一原则可以进一步扩展到身份验证。您可以直接在组件中保护您的数据,而不是依赖于可能运行也可能不运行的代码库中完全不同的中间件:

async fn require_auth(cx: &Cx) -> Result<User> {
    if let Some(Session { user_id }) = current_session(cx).await? {
        Ok(load_user(cx, user_id).await?)
    } else {
        // 数据保密,重定向到登录。
        Err(redirect("/login").into())
    }
}

#[component]
async fn user_profile(cx: &Cx) -> Result {
    // `user_profile` 在用户未登录时自我保护,防止误用!
    let user = require_auth(cx).await?;
    view! {
        <h1>(user.name)</h1>
        ...
    }
}

像 React 中的钩子(但没有可怕的 钩子规则)一样,函数通过传递请求上下文(cx)可以很好地组合。

Axum 怎么办

Axum 是另一个托管在 tokio-rs 上的用于构建服务端应用的 crate。因此,我想指出 Topcoat 和 Axum 涵盖了非常不同的用例。事实上,我预计许多 Topcoat 用户最终也会在他们的项目中使用 Axum。Axum 是一个低级的 HTTP 路由器,可以轻松构建 HTTP API 端点。您也可以使用 Axum 构建响应式 Web 应用,但这需要更多的样板代码和设置。Topcoat 旨在消除这些样板代码,但如果您只是想构建那些低级的 HTTP 端点,您仍然应该选择 Axum

接下来

这只是 Topcoat 的第一个版本。我们的路线图上有很多内容,包括与 Toasty 的更紧密集成、验证、电子邮件等。您可以在 Readme 中了解其中的一些内容。

同时,Topcoat 现在就可以使用,如果您需要数据库,请拉取 Toasty。试试看,并向我们发送问题和/或反馈。我们都在 Tokio Discord 的 #topcoat 频道中。

祝您构建愉快。