一个基础组件
A Basic Component
“Hello, world!” 那是一个非常简单的例子。让我们来做一些更像普通应用的事情。
That “Hello, world!” was a very simple example. Let’s move on to something a little more like an ordinary app.
首先,让我们修改 main 函数,使其不再渲染整个应用,而只是渲染一个 <App/> 组件。在大多数 Web 框架中,组件是组合和设计的基本单元,Leptos 也不例外。从概念上讲,它们类似于 HTML 元素:它们代表 DOM 的一个部分,具有自包含的、定义的行为。与 HTML 元素不同的是,它们采用 PascalCase(大驼峰命名法),因此大多数 Leptos 应用程序都会以类似 <App/> 的组件开始。
First, let’s edit the main function so that, instead of rendering the whole app, it just renders an <App/> component. Components are the basic unit of composition and design in most web frameworks, and Leptos is no exception. Conceptually, they are similar to HTML elements: they represent a section of the DOM, with self-contained, defined behavior. Unlike HTML elements, they are in PascalCase, so most Leptos applications will start with something like an <App/> component.
use leptos::mount::mount_to_body;
fn main() {
mount_to_body(App);
}
现在让我们定义 App 组件本身。因为它相对简单,我会先给出完整的代码,然后逐行讲解。
Now let’s define our App component itself. Because it’s relatively simple, I’ll give you the whole thing up front, then walk through it line by line.
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>
}
}
导入 Prelude
Importing the Prelude
Leptos 提供了一个 prelude(预导入模块),其中包含常用的 trait 和函数。如果你更喜欢使用单独的导入,请随意;编译器会为每个导入提供有用的建议。
Leptos provides a prelude which includes commonly-used traits and functions. If you'd prefer to use individual imports, feel free to do that; the compiler will provide helpful recommendations for each import.
组件签名
The Component Signature
与所有组件定义一样,它以 #[component] 宏开始。#[component] 标注了一个函数,使其可以在你的 Leptos 应用程序中作为组件使用。我们将在接下来的几章中看到这个宏的其他一些特性。
Like all component definitions, this begins with the #[component] macro. #[component] annotates a function so it can be used as a component in your Leptos application. We’ll see some of the other features of this macro in a couple chapters.
每个组件都是一个具有以下特性的函数:
Every component is a function with the following characteristics
-
它接受零个或多个任何类型的参数。
-
It takes zero or more arguments of any type.
-
它返回
impl IntoView,这是一种不透明类型,包含你可以从 Leptosview返回的任何内容。 -
It returns
impl IntoView, which is an opaque type that includes anything you could return from a Leptosview.
组件函数参数被聚集到一个单一的 props 结构体中,该结构体由
view宏根据需要构建。
Component function arguments are gathered together into a single props struct which is built by the
viewmacro as needed.
组件体
The Component Body
组件函数体是一个只运行一次的设置(set-up)函数,而不是多次重复运行的渲染函数。你通常会用它来创建一些响应式变量,定义响应这些值变化而运行的副作用,并描述用户界面。
The body of the component function is a set-up function that runs once, not a render function that reruns multiple times. You’ll typically use it to create a few reactive variables, define any side effects that run in response to those values changing, and describe the user interface.
signal 创建一个信号,它是 Leptos 中响应式变化和状态管理的基本单元。它返回一个 (getter, setter) 元组。要访问当前值,你将使用 count.get()(或者在 nightly Rust 上使用简写 count())。要设置当前值,你将调用 set_count.set(...)(或者在 nightly 上使用 set_count(...))。
signal creates a signal, the basic unit of reactive change and state management in Leptos. This returns a (getter, setter) tuple. To access the current value, you’ll use count.get() (or, on nightly Rust, the shorthand count()). To set the current value, you’ll call set_count.set(...) (or, on nightly, set_count(...)).
.get()会克隆值,而.set()会覆盖它。在许多情况下,使用.with()或.update()会更高效;如果你想在此时了解更多关于这些权衡的信息,请查看ReadSignal和WriteSignal的文档。
.get()clones the value and.set()overwrites it. In many cases, it’s more efficient to use.with()or.update(); check out the docs forReadSignalandWriteSignalif you’d like to learn more about those trade-offs at this point.
视图
The View
Leptos 通过 view 宏使用类似于 JSX 的格式来定义用户界面。
Leptos defines user interfaces using a JSX-like format via the view macro.
view! {
<button
// 使用 on: 定义事件监听器
// define an event listener with on:
on:click=move |_| set_count.set(3)
>
// 文本节点被包裹在引号中
// text nodes are wrapped in quotation marks
"Click me: "
// 代码块包含 Rust 代码
// blocks include Rust code
// 在这种情况下,它渲染信号的值
// in this case, it renders the value of the signal
{count}
</button>
<p>
"Double count: "
{move || count.get() * 2}
</p>
}
这大部分应该很容易理解:它看起来很像 HTML,带有一种特殊的 on:click 语法来定义 click 事件监听器,以及一些看起来像 Rust 字符串的文本节点。支持所有 HTML 元素,包括内置元素(如 <p>)和自定义元素/Web 组件(如 <my-custom-element>)。
This should mostly be easy to understand: it mostly looks like HTML, with a special on:click syntax to define a click event listener and a few text nodes that look like Rust strings. All HTML elements are supported, including both built-in elements (like <p>) and custom elements/web components (like <my-custom-element>).
view 宏确实对未加引号的文本节点提供了一些支持,这在 HTML 或 JSX 中是常态(即 <p>Hello!</p> 而不是 <p>"Hello!"</p>)。由于 Rust 过程宏的限制,使用未加引号的文本偶尔会导致标点符号周围的间距问题,并且不支持所有的 Unicode 字符串。如果你愿意,可以使用未加引号的文本;请注意,如果你遇到任何问题,始终可以通过将文本节点作为普通 Rust 字符串加引号来解决。
然后是大括号中的两个值:一个 {count} 似乎很容易理解(它只是我们信号的值),然后是……
Then there are two values in braces: one, {count}, seems pretty easy to understand (it's just the value of our signal), and then...
{move || count.get() * 2}
不管那是什么。
whatever that is.
人们有时会开玩笑说,他们在第一个 Leptos 应用程序中使用的闭包比他们一生中使用的还要多。这很有道理。
People sometimes joke that they use more closures in their first Leptos application than they’ve ever used in their lives. And fair enough.
将函数传递到视图中告诉框架:“嘿,这是可能会改变的东西。”
Passing a function into the view tells the framework: “Hey, this is something that might change.”
当我们点击按钮并调用 set_count 时,count 信号就会更新。这个 move || count.get() * 2 闭包(其值取决于 count 的值)会重新运行,框架会对该特定文本节点进行有针对性的更新,而不会触及应用程序中的其他任何内容。这就是实现 DOM 极高效更新的原因。
When we click the button and call set_count, the count signal is updated. This move || count.get() * 2 closure, whose value depends on the value of count, reruns, and the framework makes a targeted update to that specific text node, touching nothing else in your application. This is what allows for extremely efficient updates to the DOM.
记住——这一点非常重要——在视图中只有信号和函数被视为响应式值。
Remember—and this is very important—only signals and functions are treated as reactive values in the view.
这意味着 {count} 和 {count.get()} 在你的视图中执行完全不同的操作。{count} 传入一个信号,告诉框架每次 count 改变时都要更新视图。{count.get()} 则只访问一次 count 的值,并将一个 i32 传入视图,非响应式地渲染一次。
This means that {count} and {count.get()} do very different things in your view. {count} passes in a signal, telling the framework to update the view every time count changes. {count.get()} accesses the value of count once, and passes an i32 into the view, rendering it once, unreactively.
同样地,{move || count.get() * 2} 和 {count.get() * 2} 的行为也不同。第一个是函数,因此它是响应式渲染的。第二个是一个值,因此它只渲染一次,并且在 count 改变时不会更新。
In the same way, {move || count.get() * 2} and {count.get() * 2} behave differently. The first one is a function, so it's rendered reactively. The second is a value, so it's just rendered once, and won't update when count changes.
你可以在下面的 CodeSandbox 中看到区别!
You can see the difference in the CodeSandbox below!
让我们做最后一个修改。set_count.set(3) 对于点击处理程序来说是一件相当没用的事情。让我们把 “将此值设置为 3” 替换为 “将此值增加 1”:
Let’s make one final change. set_count.set(3) is a pretty useless thing for a click handler to do. Let’s replace “set this value to 3” with “increment this value by 1”:
move |_| {
*set_count.write() += 1;
}
你可以在这里看到,虽然 set_count 只是设置值,但 set_count.write() 为我们提供了一个可变引用并原地修改值。任何一种方式都会触发 UI 中的响应式更新。
You can see here that while set_count just sets the value, set_count.write() gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI.
在整个教程中,我们将使用 CodeSandbox 来展示交互式示例。将鼠标悬停在任何变量上以显示 Rust-Analyzer 详情和正在发生的事情的文档。欢迎 fork 这些示例来自己动手尝试!
Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. Hover over any of the variables to show Rust-Analyzer details and docs for what’s going on. Feel free to fork the examples to play with them yourself!
在线示例
在线示例
要在沙盒中显示浏览器,你可能需要点击
Add DevTools > Other Previews > 8080.To show the browser in the sandbox, you may need to clickAdd DevTools > Other Previews > 8080.
CodeSandbox Source
use leptos::prelude::*;
// #[component] 宏将一个函数标记为可复用组件
// The #[component] macro marks a function as a reusable component
// 组件是用户界面的构建块
// Components are the building blocks of your user interface
// 它们定义了一个可复用的行为单元
// They define a reusable unit of behavior
#[component]
fn App() -> impl IntoView {
// 在这里我们创建一个响应式信号
// here we create a reactive signal
// 并得到一个 (getter, setter) 对
// and get a (getter, setter) pair
// 信号是框架中变化的基本单元
// signals are the basic unit of change in the framework
// 我们稍后会详细讨论它们
// we'll talk more about them later
let (count, set_count) = signal(0);
// view! 宏是我们定义用户界面的方式
// the `view` macro is how we define the user interface
// 它使用一种类似于 HTML 的格式,可以接受特定的 Rust 值
// it uses an HTML-like format that can accept certain Rust values
view! {
<button
// 每当 click 事件触发时,on:click 就会运行
// on:click will run whenever the `click` event fires
// 每个事件处理程序都定义为 on:{eventname}
// every event handler is defined as `on:{eventname}`
// 我们能够将 set_count 移动到闭包中
// we're able to move `set_count` into the closure
// 因为信号是 Copy 且具有 'static 生命周期的
// because signals are Copy and 'static
on:click=move |_| *set_count.write() += 1
>
// RSX 中的文本节点应该用引号包裹,
// text nodes in RSX should be wrapped in quotes,
// 就像普通的 Rust 字符串一样
// like a normal Rust string
"Click me: "
{count}
</button>
<p>
<strong>"Reactive: "</strong>
// 你可以将 Rust 表达式作为值插入 DOM
// you can insert Rust expressions as values in the DOM
// 通过将它们包裹在大括号中
// by wrapping them in curly braces
// 如果你传入一个函数,它将响应式地更新
// if you pass in a function, it will reactively update
{move || count.get()}
</p>
<p>
<strong>"Reactive shorthand: "</strong>
// 你可以直接在视图中使用信号,作为
// you can use signals directly in the view, as a shorthand
// 仅包装 getter 的函数的简写
// for a function that just wraps the getter
{count}
</p>
<p>
<strong>"Not reactive: "</strong>
// 注意:如果你只是写 {count.get()},这 *不会* 是响应式的
// NOTE: if you just write {count.get()}, this will *not* be reactive
// 它只是获取一次 count 的值
// it simply gets the value of count once
{count.get()}
</p>
}
}
// 这个 main 函数是应用程序的入口点
// This `main` function is the entry point into the app
// 它只是将我们的组件挂载到 <body>
// It just mounts our component to the <body>
// 因为我们将它定义为 fn App,所以我们现在可以在
// Because we defined it as `fn App`, we can now use it in a
// 模板中以 <App/> 的形式使用它
// template as <App/>
fn main() {
leptos::mount::mount_to_body(App)
}