返回首页

Leptos使用体验

Leptos使用体验

Leptos是什么?

​ Leptos是一个全栈、同构的Rust Web框架,全栈是指它包含前后端,没有前后端分离带来的CORS等问题,同构是指他的前后端语言都是Rust。其采用的是组件化和细粒度的响应式系统,当你在Leptos 中修改了一个Signal(信号),比如一个值从1改为2,他会直接精确的找到其DOM文本节点或元素属性进行跟新,这样子做使得计算开销几乎为0,即“我知道谁依赖这个值,我只通知它”。

上面提到的Signal(信号),是响应式系统中的基本原子单位,可以理解为持有某个值,并能通知所有“订阅者”该值已发生变化的特殊对象。以下是一个简单的计数器实现

let (count, set_count) = create_signal(0
view! {
    <button on:click=move |_| set_count.set(count.get() + 1)>
        "点击次数: " {move || count.get()}
    </button>
}

当这段代码首次渲染时,框架会执行 move || count.get() 这个闭包,并自动记录:“这个按钮的文字内容,依赖于 count 这个 Signal。”

当你点击按钮,执行 set_count.set(...) 时:

  1. 框架不会重新运行整个组件函数。
  2. 框架翻出之前的“订阅名单”,发现只有这个按钮的文字依赖 count。
  3. 框架直接调用浏览器的 DOM API(textContent = "点击次数: 1"),修改了这一个小节点。

整个过程中,组件函数没有重新执行,虚拟 DOM 没有创建,没有任何 Diff 对比

如React等粗粒度的响应式系统,在你修改了组件里的一个状态时,会重新执行整个组件函数,它会生成一份全新的虚拟 DOM(Virtual DOM),然后与旧的虚拟 DOM 进行全量对比(Diff),最后找出变化的地方,更新到真实的浏览器 DOM 上。这是其性能优于其他前端的原因之一。

Leptos 提供了灵活的渲染模式,你可以根据项目需求选择:

  • 客户端渲染 (CSR):应用完全在浏览器中作为单页应用 (SPA) 运行,服务器只发一个空的 <div id="root"></div> 和巨大的 JS/WASM 包。浏览器先下载代码,执行代码,然后在内存里生成虚拟 DOM,最后把元素挂载到页面上。这种方式构建速度快,开发迭代迅速,但是首屏加载慢(白屏时间长),用户要等代码下载完才能看到东西。
  • 服务器端渲染 (SSR):应用在服务器端渲染成 HTML 后发送到浏览器,然后在客户端通过 WebAssembly 进行“注水”(Hydration)使其具备交互性。这种方式能提供更快的首屏加载速度和更好的 SEO。

“注水”(Hydration0)是 Leptos 全栈模式的默认做法:

他结合了SSR和CSR两者

  1. 第一次访问(服务器SSR部分):Leptos 在服务器上运行组件,生成完整的静态 HTML 字符串,立刻发给浏览器。用户瞬间看到完整页面。
  2. 第二次加载(客户端CSR部分):浏览器在后台下载 Leptos 的 WASM 包。下载完成后,WASM 代码开始执行。代码执行时,先扫描整个 HTML 树,找到每一个 DOM 节点,将事件监听器(on:click 等)和响应式 Signal 绑定上去**,不会重新创建所有的 DOM 节点(因为已经有了)。

这个“扫描并绑定事件”的过程,就叫 “注水”。

除了上面的优点,Leptos有一个极大的缺点,就是其专注于纯Web应用的场景,它无法做到一份代码在小程序,iOS / Android App,Web 网页端等多端运行并展现通用的页面,这在目前的时代是一个极大的缺点。以前我们往前后端分离的开发模式进行发展,就是为了在业务场景下进行解耦,从前为了适配多端,后端需要为每个终端写一套渲染逻辑,前端等后端逻辑写完才能进行开发,而前后端分离之后,后端只需要传输纯数据就行,同时前后还能和后端并行开发,只需要通过接口文档约定好契约就行。

实战部分

​ 本博客前后端就是用Leptos搭建,主要讲几个实战遇到的问题。

1. SSR 只输出"加载中..."

现象

curl 抓首页 HTML,正文里只有 <p class="list-state">加载中...</p>,文章列表要等 WASM 水合后才出现。禁用 JS 则页面永远空白,SEO 也拿不到内容。这在技术上有一个专有名词,叫 “客户端水合后的二次数据获取”

排查

  • 抓包发现资源数据其实已经序列化在页面尾部的 __RESOLVED_RESOURCES 里,但正文渲染的是 pending 状态。
  • 服务器控制台反复出现警告: reading a resource in hydrate mode outside a <Suspense/> or <Transition/> or effect

根因

Resource是 Leptos 的异步数据源,页面在渲染时数据可能还没到,就是告诉框架这一个块在等异步数据,可以先展示fallback。只有在 <Suspense> 子树内被读取时,SSR 才会阻塞等待它解析并渲染真实内容。项目里所有 Resource 都是直接在视图里裸读,SSR 阶段拿到的永远是 None(加载中)。

代码

let posts = Resource::new(|| (), |_| async move { list_posts().await });

view! {
    <section class="posts" aria-labelledby="latest-heading">
    <div class="section-head">
    <h2 id="latest-heading">"最新文章"</h2>
    <a class="section-head__more" href="/archive">"全部文章"</a>
    </div>
    // 之前(错误)
    {move || match posts.get() { ... }}

    // 之后(正确)
    <Suspense fallback=|| view! { <p class="list-state">"加载中..."</p> }>
    {move || match posts.get() { ... }}
    </Suspense>

    </section>
}

验证

  • 修复前 HTML 无内容模板;修复后文章列表/正文直接内联在 HTML 中。
  • 真实浏览器(Playwright)+ hydration 下 5 个路由控制台零警告。

2. 缺失文章返回 200 而非 404

现象

/posts/not-exist 返回 200,但页面显示"文章不存在"。

排查

代码里明明调用了 ResponseOptions::set_status(NOT_FOUND),为何没生效?读了 leptos_axum 源码 from_app:

// 等待流的第一个 chunk,然后应用状态码和响应头
let first_chunk = stream.next().await.unwrap_or_default();
res.extend_response(&res_options);

根因

路由渲染是流式的(to_html_stream_in_order):shell 的第一个 chunk 一产生,状态码就随响应头发出了。而 404 要等 get_post 查完库才知道,属于"晚到"的状态设置——此时响应头已提交,改了也没用。文档里也明确写了:流式渲染下非阻塞资源里的 redirect/状态码修改无效。

解决

文章路由改用 SsrMode::Async——这种模式会等所有资源解析完、整体渲染完毕后才发响应:

<Route path=path!("/posts/:slug") view=PostPage ssr=SsrMode::Async/>

验证

curl -w "%{http_code}" 实测缺失文章返回 404。额外收获:Async 模式下文章页 <title> 也能正确带上文章名(流式模式下 head 已发出,meta 更新无效)。

一些函数的用法

ActionFrom:

提交自动触发函数

示例代码:

//AddComment 为server注册的endpoint方法 采用驼峰命名
let add_comment = ServerAction::<AddComment>::new();
//自动触发add_comment函数
<ActionForm action=add_comment>
</ActionForm>

Effect用途:

• 值变化检测:信号每次 set 都可能触发 effect,但用 prev 对比就能只在“真的变了“时做事 • 状态传递:在两次运行之间携带数据,不用额外加变量

示例代码:

Effect::new(move |_| {
    //追踪add_comment.value()值的变化
    if add_comment.value().get().is_some_and(|r| r.is_ok()) {
        comments_version.update(|v| *v += 1);/
        draft.set(String::new());
        reply_to.set(None);
        add_comment.value().set(None);
    }
});

结尾

​ Leptos的开发体验还是挺不错的,在前端界面上我还写了骨架屏过度动画,但在Leptos的性能和我这微小的业务场景上骨架的显示只有几帧,感觉是多此一举了哈哈,关于Leptos跨平台的想法,可以借助Tauri等框架,但在兼容性和成熟度上不如已经比较成熟的Vue和React,追求性能和低延迟的可以考虑使用。

评论2