基础组件
那个 "Hello, world!" 是一个_非常_简单的例子。让我们转向更像普通应用程序的东西。
首先,让我们编辑 main 函数,使其不渲染整个应用程序,而只是渲染一个 <App/> 组件。组件是大多数 Web 框架中组合和设计的基本单元,Leptos 也不例外。从概念上讲,它们类似于 HTML 元素:它们代表 DOM 的一个部分,具有自包含的、定义的行为。与 HTML 元素不同,它们使用 PascalCase,所以大多数 Leptos 应用程序都会从类似 <App/> 组件的东西开始。
use leptos::mount::mount_to_body;
fn main() {
mount_to_body(App);
}
现在让我们定义我们的 App 组件本身。因为它相对简单,我会先给您完整的代码,然后逐行解释。
use leptos::prelude::*;
#[component]
fn App() -> impl IntoView {
let (count, set_count) = signal(0);
view! {
<button
on:click=move |_| set_count.set(3)
>
"Click me: "
{count}
</button>
<p>
"Double count: "
{move || count.get() * 2}
</p>
}
}
内容详解
第一部分:入口函数 (main)
这一段代码告诉程序:从哪里开始运行,以及要把网页“挂载”到哪里。
use leptos::mount::mount_to_body; // 1. 引入工具
fn main() { // 2. 主函数入口
mount_to_body(App); // 3. 把 App 组件挂载到网页身体上
}
语法解释:
use ...;- 通俗解释:相当于“从工具箱里拿工具”。
- 作用:Rust 的功能都在不同的模块(module)里。这句话的意思是:“我要使用
leptos这个工具箱里的mount盒子里的mount_to_body这个工具。”
fn main() { ... }- 通俗解释:这是程序的“总指挥”或“大门”。
- 语法:
fn是function(函数)的缩写。Rust 程序无论多复杂,都是从main函数开始执行的。
App- 这里提到的
App是我们在后面定义的“组件”。在 Leptos 里,组件通常用大驼峰命名法(PascalCase,即首字母大写,如App),这和普通的 HTML 标签(如div,p)区分开来。
- 这里提到的
第二部分:定义组件 (App)
这是核心部分。作者给了你一个完整的组件代码。我们可以把它看作是一个自定义的积木块。
use leptos::prelude::*; // 引入常用的工具
#[component] // 1. 这是一个组件的“标签”
fn App() -> impl IntoView { // 2. 定义函数,返回“视图”
let (count, set_count) = signal(0); // 3. 创建一个“信号”(状态)
// 4. 定义长什么样 (UI)
view! {
<button
on:click=move |_| set_count.set(3) // 5. 点击事件
>
"Click me: "
{count} // 6. 显示数据
</button>
<p>
"Double count: "
{move || count.get() * 2} // 7. 衍生数据
</p>
}
}
逐行语法与逻辑详解:
1. #[component] —— 宏属性 (Attribute Macro)
- 语法:
#[...]写在函数上面。 - 通俗解释:这就像给下面的函数贴了一张特权贴纸。
- 作用:它告诉 Rust 编译器:“嘿,下面的
fn App不是普通函数,它是一个 UI 组件!”编译器看到这张贴纸后,会在后台悄悄帮我们写很多复杂的代码,让我们只要写简单的逻辑就行。
2. fn App() -> impl IntoView —— 函数签名
- 语法:
->后面跟的是返回值类型。 impl IntoView:- 通俗解释:你可以把它理解为“某种可以被显示出来的东西”。
- 作用:Rust 是强类型语言,通常要求你精确说出返回什么(比如“一个整数”)。但 UI 结构很复杂,很难精确描述。
impl IntoView的意思就是:“我不告诉你具体的复杂类型,但我向你保证,这个函数返回的东西一定是可以被画在屏幕上的。”
3. let (count, set_count) = signal(0); —— 响应式信号
这是 Leptos 最核心的概念:Signal(信号)。
- 语法
let (a, b) = ...:这叫“解构”。因为signal(0)生产了一对东西,我们把它们分别起名为count和set_count。 - 通俗解释:
- 这就好比你在墙上装了一个数字显示器。
0:初始数字是 0。count(读信号):这是眼睛。当你需要看现在的数字是多少时,用它。set_count(写信号):这是遥控器。当你需要改数字时,用它。
- 重点:为什么不直接用变量
let count = 0?因为普通变量改了之后,网页界面不会跟着变。而用signal,一旦你用遥控器改了数字,网页上所有用到这个数字的地方都会自动更新。
4. view! { ... } —— 视图宏
- 语法:
view!后面跟着花括号{ ... }。 - 通俗解释:这是 Rust 里的HTML 魔法传送门。
- 作用:Rust 本身不懂 HTML(比如
<button>)。这个view!宏让你可以直接在 Rust 代码里写类似 HTML 的代码,它会负责把它翻译成真正的网页元素。
5. on:click=move |_| set_count.set(3) —— 事件与闭包
这一行信息量很大,是初学者最头疼的地方。
on:click:这是给按钮绑定点击事件。move |_| ...:这在 Rust 里叫闭包 (Closure)。- 通俗解释:这是一个匿名的小函数,也就是“当点击发生时要执行的任务”。
|_|:竖线里放的是参数。点击事件通常会传给你一个“事件对象”(包含鼠标位置等),用_表示“我知道有个参数,但我不在乎它,也不打算用它”。move:这是 Rust 特有的关键字。- 比喻:这个小任务(闭包)需要用到外面的
set_count(遥控器)。但是这个任务可能在很久以后(用户点击时)才执行。move的意思就是:“把这个遥控器的所有权或者复印件打包带走,装进这个小任务的背包里,确保将来要用的时候它还在。”
- 比喻:这个小任务(闭包)需要用到外面的
set_count.set(3):使用遥控器,把数字强制设为 3。
6. {count} —— 变量嵌入
- 语法:在
view!里的花括号{}。 - 作用:告诉
view!:“这里不是普通的文字,这里是 Rust 的代码/变量,请计算它的值放这里。” - 逻辑:这里直接放入了
count(读信号)。Leptos 很聪明,因为它是个信号,所以只要数字变了,这里的文字就会自动变。
7. {move || count.get() * 2} —— 衍生信号(计算属性)
这里展示了如果显示的数字需要计算该怎么写。
- 语法
move || ...:又是一个闭包(小函数)。||:这里没有参数,所以竖线是空的。
- 通俗解释:这里不能直接写
count * 2,因为我们需要告诉框架:“这是一个动态的公式,不是一个死数字。” - 逻辑:
- 这是一个小函数:
|| count.get() * 2(获取当前值,乘以 2)。 - 因为它用到了
count,所以当count变化时,这块区域也会自动重新计算并更新。 - 加上
move同样是为了把count这个工具打包带进这个公式里使用。
- 这是一个小函数:
总结整个流程
- 程序启动,挂载
App组件。 App组件初始化,创建了一个数字信号,初始为0。- 画出界面:
- 一个按钮,上面写着 "Click me: " 和当前的数字(0)。
- 一段文字,写着 "Double count: " 和当前数字的两倍(0)。
- 当你点击按钮:
- 触发点击事件。
- 执行那个“小任务”(闭包):拿出遥控器
set_count,把值设为3。
- 自动更新:
- 信号变了!Leptos 框架发现
count变了。 - 它自动找到界面上所有用到
count的地方。 - 按钮上的字变成了 "Click me: 3"。
- 下面的段落自动重新计算
3 * 2,变成了 "Double count: 6"。
- 信号变了!Leptos 框架发现
导入 Prelude
use leptos::prelude::*;
Leptos 提供了一个 prelude,其中包括常用的 traits 和函数。如果您更愿意使用单独的导入,请随意这样做;编译器将为每个导入提供有用的建议。
组件签名
#[component]
像所有组件定义一样,这以 #[component] 宏开始。#[component] 注释一个函数,使其可以在您的 Leptos 应用程序中用作组件。我们将在几章后看到这个宏的其他一些功能。
fn App() -> impl IntoView
每个组件都是具有以下特征的函数:
- 它接受零个或多个任何类型的参数。
- 它返回
impl IntoView,这是一个不透明类型,包括您可以从 Leptosview返回的任何内容。
组件函数参数被收集到一个单一的 props 结构体中,该结构体由
view宏根据需要构建。
组件主体
组件函数的主体是一个运行一次的设置函数,而不是多次重新运行的渲染函数。您通常会使用它来创建一些响应式变量,定义响应这些值变化而运行的任何副作用,并描述用户界面。
let (count, set_count) = signal(0);
signal 创建一个 signal,这是 Leptos 中响应式变化和状态管理的基本单元。这返回一个 (getter, setter) 元组。要访问当前值,您将使用 count.get()(或者,在 nightly Rust 上,简写 count())。要设置当前值,您将调用 set_count.set(...)(或者,在 nightly 上,set_count(...))。
.get()克隆值,.set()覆盖它。在许多情况下,使用.with()或.update()更高效;如果您想在此时了解更多关于这些权衡的信息,请查看ReadSignal和WriteSignal的文档。
视图
Leptos 通过 view 宏使用类似 JSX 的格式定义用户界面。
view! {
<button
// 使用 on: 定义事件监听器
on:click=move |_| set_count.set(3)
>
// 文本节点用引号包装
"Click me: "
// 块包含 Rust 代码
// 在这种情况下,它渲染 signal 的值
{count}
</button>
<p>
"Double count: "
{move || count.get() * 2}
</p>
}
这应该大部分都容易理解:它看起来主要像 HTML,有一个特殊的 on:click 语法来定义 click 事件监听器和一些看起来像 Rust 字符串的文本节点。支持所有 HTML 元素,包括内置元素(如 <p>)和自定义元素/Web 组件(如 <my-custom-element>)。
无引号文本:view 宏确实对无引号文本节点有一些支持,这在 HTML 或 JSX 中是常见的(即 <p>Hello!</p> 而不是 <p>"Hello!"</p>)。由于 Rust proc 宏的限制,使用无引号文本偶尔会在标点符号周围造成间距问题,并且不支持所有 Unicode 字符串。如果这是您的偏好,您可以使用无引号文本;请注意,如果您遇到任何问题,总是可以通过将文本节点引用为普通 Rust 字符串来解决。
然后有两个大括号中的值:一个,{count},似乎很容易理解(它只是我们 signal 的值),然后...
{move || count.get() * 2}
不管那是什么。
人们有时开玩笑说,他们在第一个 Leptos 应用程序中使用的闭包比他们一生中使用的都多。这很公平。
将函数传递到视图中告诉框架:"嘿,这是可能会改变的东西。"
当我们点击按钮并调用 set_count 时,count signal 被更新。这个 move || count.get() * 2 闭包,其值依赖于 count 的值,重新运行,框架对该特定文本节点进行有针对性的更新,不触及应用程序中的任何其他内容。这就是允许对 DOM 进行极其高效更新的原因。
记住——这_非常重要_——只有 signals 和函数在视图中被视为响应式值。
这意味着 {count} 和 {count.get()} 在您的视图中做非常不同的事情。{count} 传入一个 signal,告诉框架每次 count 更改时更新视图。{count.get()} 访问 count 的值一次,并将 i32 传递到视图中,渲染一次,非响应式地。
同样,{move || count.get() * 2} 和 {count.get() * 2} 的行为不同。第一个是函数,所以它是响应式渲染的。第二个是值,所以它只渲染一次,当 count 更改时不会更新。
您可以在下面的 CodeSandbox 中看到差异!
让我们做最后一个更改。set_count.set(3) 对于点击处理程序来说是一个相当无用的事情。让我们将"将此值设置为 3"替换为"将此值增加 1":
move |_| {
*set_count.write() += 1;
}
您可以在这里看到,虽然 set_count 只是设置值,set_count.write() 给我们一个可变引用并就地改变值。任何一个都会在我们的 UI 中触发响应式更新。
在整个教程中,我们将使用 CodeSandbox 来显示交互式示例。 悬停在任何变量上以显示 Rust-Analyzer 详细信息 和正在发生的事情的文档。随时 fork 示例来自己玩!
CodeSandbox 源码
use leptos::prelude::*;
// #[component] 宏将函数标记为可重用组件
// 组件是用户界面的构建块
// 它们定义了一个可重用的行为单元
#[component]
fn App() -> impl IntoView {
// 在这里我们创建一个响应式 signal
// 并获得一个 (getter, setter) 对
// signals 是框架中变化的基本单元
// 我们稍后会更多地讨论它们
let (count, set_count) = signal(0);
// `view` 宏是我们定义用户界面的方式
// 它使用类似 HTML 的格式,可以接受某些 Rust 值
view! {
<button
// on:click 将在 `click` 事件触发时运行
// 每个事件处理程序都定义为 `on:{eventname}`
// 我们能够将 `set_count` 移动到闭包中
// 因为 signals 是 Copy 和 'static 的
on:click=move |_| *set_count.write() += 1
>
// RSX 中的文本节点应该用引号包装,
// 像普通的 Rust 字符串
"Click me: "
{count}
</button>
<p>
<strong>"Reactive: "</strong>
// 您可以通过将 Rust 表达式包装在大括号中
// 将它们作为值插入到 DOM 中
// 如果您传入一个函数,它将响应式更新
{move || count.get()}
</p>
<p>
<strong>"Reactive shorthand: "</strong>
// 您可以直接在视图中使用 signals,作为
// 只包装 getter 的函数的简写
{count}
</p>
<p>
<strong>"Not reactive: "</strong>
// 注意:如果您只写 {count.get()},这将*不*是响应式的
// 它只是获取 count 的值一次
{count.get()}
</p>
}
}
// 这个 `main` 函数是应用程序的入口点
// 它只是将我们的组件挂载到 <body>
// 因为我们将其定义为 `fn App`,我们现在可以在
// 模板中使用它作为 <App/>
fn main() {
leptos::mount::mount_to_body(App)
}