组件与 Props
Components and Props
到目前为止,我们一直是在单个组件中构建整个应用程序。对于非常微小的示例来说这没问题,但在任何实际应用中,你都需要将用户界面拆分为多个组件,以便将界面分解为更小、可复用、可组合的块。
So far, we’ve been building our whole application in a single component. This is fine for really tiny examples, but in any real application you’ll need to break the user interface out into multiple components, so you can break your interface down into smaller, reusable, composable chunks.
以我们的进度条为例。假设你想要两个进度条而不是一个:一个点击一次前进一格,另一个点击一次前进两格。
Let’s take our progress bar example. Imagine that you want two progress bars instead of one: one that advances one tick per click, one that advances two ticks per click.
你可以通过创建两个 <progress> 元素来实现:
You could do this by just creating two <progress> elements:
let (count, set_count) = signal(0);
let double_count = move || count.get() * 2;
view! {
<progress
max="50"
value=count
/>
<progress
max="50"
value=double_count
/>
}
但当然,这种方式的扩展性并不好。如果你想添加第三个进度条,你需要再次添加这段代码。如果你想修改它的任何内容,你需要修改三处。
But of course, this doesn’t scale very well. If you want to add a third progress bar, you need to add this code another time. And if you want to edit anything about it, you need to edit it in triplicate.
相反,让我们创建一个 <ProgressBar/> 组件。
Instead, let’s create a <ProgressBar/> component.
#[component]
fn ProgressBar() -> impl IntoView {
view! {
<progress
max="50"
// 嗯……我们要从哪里获取这个值呢?
// hmm... where will we get this from?
value=progress
/>
}
}
现在只有一个问题:progress 未定义。它应该从哪里来?当我们手动定义一切时,我们直接使用局部变量名。现在我们需要某种方式将参数传递到组件中。
There’s just one problem: progress is not defined. Where should it come from? When we were defining everything manually, we just used the local variable names. Now we need some way to pass an argument into the component.
组件 Props
Component Props
我们通过组件属性(properties)或 “props” 来实现这一点。如果你使用过其他前端框架,这可能是一个熟悉的概念。基本上,属性之于组件就像特性(attributes)之于 HTML 元素:它们允许你将额外的信息传递到组件中。
We do this using component properties, or “props.” If you’ve used another frontend framework, this is probably a familiar idea. Basically, properties are to components as attributes are to HTML elements: they let you pass additional information into the component.
在 Leptos 中,你通过为组件函数提供额外的参数来定义 props。
In Leptos, you define props by giving additional arguments to the component function.
#[component]
fn ProgressBar(
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
<progress
max="50"
// 现在这可以工作了
// now this works
value=progress
/>
}
}
现在我们可以在主 <App/> 组件的视图中使用我们的组件了。
Now we can use our component in the main <App/> component’s view.
#[component]
fn App() -> impl IntoView {
let (count, set_count) = signal(0);
view! {
<button on:click=move |_| *set_count.write() += 1>
"Click me"
</button>
// 现在使用我们的组件!
// now we use our component!
<ProgressBar progress=count/>
}
}
在视图中使用组件看起来非常像使用 HTML 元素。你会注意到你可以很容易地分辨出元素和组件之间的区别,因为组件名总是采用 PascalCase(大驼峰命名法)。你像传递 HTML 元素特性一样传入 progress prop。很简单。
Using a component in the view looks a lot like using an HTML element. You’ll notice that you can easily tell the difference between an element and a component because components always have PascalCase names. You pass the progress prop in as if it were an HTML element attribute. Simple.
响应式和静态 Props
Reactive and Static Props
你会注意到在整个示例中,progress 接收的是响应式的 ReadSignal<i32>,而不是普通的 i32。这非常重要。
You’ll notice that throughout this example, progress takes a reactive ReadSignal<i32>, and not a plain i32. This is very important.
组件 props 本身没有附加任何特殊含义。组件只是一个运行一次以设置用户界面的函数。告诉界面响应变化的唯一方法是向其传递信号(signal)类型。因此,如果你有一个会随时间变化的组件属性(如我们的 progress),它应该是一个信号。
Component props have no special meaning attached to them. A component is simply a function that runs once to set up the user interface. The only way to tell the interface to respond to changes is to pass it a signal type. So if you have a component property that will change over time, like our progress, it should be a signal.
optional Props
optional Props
目前 max 设置是硬编码的。让我们也把它作为一个 prop。但让我们把这个 prop 设为可选的。我们可以通过使用 #[prop(optional)] 进行标注来实现。
Right now the max setting is hard-coded. Let’s take that as a prop too. But let’s make this prop optional. We can do that by annotating it with #[prop(optional)].
#[component]
fn ProgressBar(
// 将此 prop 标记为可选
// mark this prop optional
// 使用 <ProgressBar/> 时你可以指定它,也可以不指定
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
<progress
max=max
value=progress
/>
}
}
现在,我们可以使用 <ProgressBar max=50 progress=count/>,也可以省略 max 来使用默认值(即 <ProgressBar progress=count/>)。optional 的默认值是其类型的 Default::default() 值,对于 u16 来说是 0。对于进度条来说,最大值为 0 并不是很有用。
Now, we can use <ProgressBar max=50 progress=count/>, or we can omit max to use the default value (i.e., <ProgressBar progress=count/>). The default value on an optional is its Default::default() value, which for a u16 is going to be 0. In the case of a progress bar, a max value of 0 is not very useful.
因此,让我们给它一个特定的默认值。
So let’s give it a particular default value instead.
default props
default props
你可以通过 #[prop(default = ...) 相当简单地指定 Default::default() 以外的默认值。
You can specify a default value other than Default::default() pretty simply with #[prop(default = ...).
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
<progress
max=max
value=progress
/>
}
}
泛型 Props
Generic Props
这很好。但我们开始时有两个计数器,一个由 count 驱动,另一个由派生信号 double_count 驱动。让我们通过在另一个 <ProgressBar/> 上使用 double_count 作为 progress prop 来重现这一点。
This is great. But we began with two counters, one driven by count, and one by the derived signal double_count. Let’s recreate that by using double_count as the progress prop on another <ProgressBar/>.
#[component]
fn App() -> impl IntoView {
let (count, set_count) = signal(0);
let double_count = move || count.get() * 2;
view! {
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
<ProgressBar progress=count/>
// 添加第二个进度条
// add a second progress bar
<ProgressBar progress=double_count/>
}
}
嗯……这无法编译。理解原因应该很容易:我们已经声明 progress prop 接收 ReadSignal<i32>,而 double_count 不是 ReadSignal<i32>。正如 rust-analyzer 会告诉你的,它的类型是 || -> i32,即它是一个返回 i32 的闭包。
Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared that the progress prop takes ReadSignal<i32>, and double_count is not ReadSignal<i32>. As rust-analyzer will tell you, its type is || -> i32, i.e., it’s a closure that returns an i32.
处理这个问题有几种方法。一种是说:“好吧,我知道为了让视图具有响应性,它需要接收一个函数或信号。我总是可以通过将信号包装在闭包中来将其转为函数……也许我可以接收任何函数?”
There are a couple ways to handle this. One would be to say: “Well, I know that for the view to be reactive, it needs to take a function or a signal. I can always turn a signal into a function by wrapping it in a closure... Maybe I could just take any function?”
如果你正在使用带有 nightly 特性的 nightly Rust,信号本身就是函数,因此你可以使用泛型组件并接收任何 Fn() -> i32:
If you’re using nightly Rust with the nightly feature, signals are functions, so you could use a generic component and take any Fn() -> i32:
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
progress: impl Fn() -> i32 + Send + Sync + 'static
) -> impl IntoView {
view! {
<progress
max=max
value=progress
/>
// 添加换行以避免重叠
// Add a line-break to avoid overlap
<br/>
}
}
泛型 props 也可以使用
where子句指定,或者使用内联泛型如ProgressBar<F: Fn() -> i32 + 'static>。
Generic props can also be specified using a
whereclause, or using inline generics likeProgressBar<F: Fn() -> i32 + 'static>.
泛型必须在组件 props 的某处被使用。这是因为 props 被构建成一个结构体,所以所有的泛型类型都必须在结构体中的某处被使用。这通常可以通过使用一个可选的 PhantomData prop 轻松实现。然后你可以使用表达类型的语法在视图中指定泛型:<Component<T>/>(不是 turbofish 风格的 <Component::<T>/>)。
Generics need to be used somewhere in the component props. This is because props are built into a struct, so all generic types must be used somewhere in the struct. This is often easily accomplished using an optional PhantomData prop. You can then specify a generic in the view using the syntax for expressing types: <Component<T>/> (not with the turbofish-style <Component::<T>/>).
#[component]
fn SizeOf<T: Sized>(#[prop(optional)] _ty: PhantomData<T>) -> impl IntoView {
std::mem::size_of::<T>()
}
#[component]
pub fn App() -> impl IntoView {
view! {
<SizeOf<usize>/>
<SizeOf<String>/>
}
}
请注意存在一些限制。例如,我们的视图宏解析器无法处理嵌套泛型,如
<SizeOf<Vec<T>>/>。
Note that there are some limitations. For example, our view macro parser can’t handle nested generics like
<SizeOf<Vec<T>>/>.
into Props
into Props
如果你在 stable Rust 上,信号并不直接实现 Fn()。我们可以将信号包装在闭包中(move || progress.get()),但这有点繁琐。
If you’re on stable Rust, signals don’t directly implement Fn(). We could wrap the signal in a closure (move || progress.get()) but that’s a bit messy.
我们还有另一种实现方式,那就是使用 #[prop(into)]。该属性会自动对你作为 prop 传入的值调用 .into(),这使你能够轻松传递具有不同类型值的 props。
There’s another way we could implement this, and it would be to use #[prop(into)]. This attribute automatically calls .into() on the values you pass as props, which allows you to easily pass props with different values.
在这种情况下,了解一下 Signal 类型很有帮助。Signal 是一个枚举类型,代表任何类型的可读响应式信号或普通值。当你为想要复用的组件定义 API,同时又想传入不同种类的信号时,它非常有用。
In this case, it’s helpful to know about the Signal type. Signal is an enumerated type that represents any kind of readable reactive signal, or a plain value. It can be useful when defining APIs for components you’ll want to reuse while passing different sorts of signals.
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
#[prop(into)]
progress: Signal<i32>
) -> impl IntoView
{
view! {
<progress
max=max
value=progress
/>
<br/>
}
}
#[component]
fn App() -> impl IntoView {
let (count, set_count) = signal(0);
let double_count = move || count.get() * 2;
view! {
<button on:click=move |_| *set_count.write() += 1>
"Click me"
</button>
// .into() 将 `ReadSignal` 转换为 `Signal`
// .into() converts `ReadSignal` to `Signal`
<ProgressBar progress=count/>
// 使用 `Signal::derive()` 将派生信号包装为 `Signal` 类型
// use `Signal::derive()` to wrap a derived signal with the `Signal` type
<ProgressBar progress=Signal::derive(double_count)/>
}
}
可选泛型 Props
Optional Generic Props
请注意,你不能为组件指定可选的泛型 props。让我们看看如果你尝试这样做会发生什么:
Note that you can’t specify optional generic props for a component. Let’s see what would happen if you try:
#[component]
fn ProgressBar<F: Fn() -> i32 + Send + Sync + 'static>(
#[prop(optional)] progress: Option<F>,
) -> impl IntoView {
progress.map(|progress| {
view! {
<progress
max=100
value=progress
/>
<br/>
}
})
}
#[component]
pub fn App() -> impl IntoView {
view! {
<ProgressBar/>
}
}
Rust 会贴心地给出错误:
Rust helpfully gives the error
xx | <ProgressBar/>
| ^^^^^^^^^^^ 无法推断函数 `ProgressBar` 上声明的类型参数 `F` 的类型
| ^^^^^^^^^^^ cannot infer type of the type parameter `F` declared on the function `ProgressBar`
|
帮助:考虑指定泛型参数
help: consider specifying the generic argument
|
xx | <ProgressBar::<F>/>
| +++++
你可以使用 <ProgressBar<F>/> 语法(view 宏中没有 turbofish)在组件上指定泛型。在这里指定正确的类型是不可能的;闭包和一般函数是不可名状的类型。编译器可以用简写显示它们,但你无法指定它们。
You can specify generics on components with a <ProgressBar<F>/> syntax (no turbofish in the view macro). Specifying the correct type here is not possible; closures and functions in general are unnameable types. The compiler can display them with a shorthand, but you can’t specify them.
但是,你可以通过使用 Box<dyn _> 或 &dyn _ 提供具体类型来绕过这个问题:
However, you can get around this by providing a concrete type using Box<dyn _> or &dyn _:
#[component]
fn ProgressBar(
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32 + Send + Sync>>,
) -> impl IntoView {
progress.map(|progress| {
view! {
<progress
max=100
value=progress
/>
<br/>
}
})
}
#[component]
pub fn App() -> impl IntoView {
view! {
<ProgressBar/>
}
}
由于 Rust 编译器现在知道 prop 的具体类型,因此即使在 None 的情况下也知道它在内存中的大小,所以这段代码可以正常编译。
Because the Rust compiler now knows the concrete type of the prop, and therefore its size in memory even in the None case, this compiles fine.
在这种特定情况下,
&dyn Fn() -> i32会导致生命周期问题,但在其他情况下,它可能是一种选择。
In this particular case,
&dyn Fn() -> i32will cause lifetime issues, but in other cases, it may be a possibility.
组件文档
Documenting Components
这是本书中最不重要但又最重要的部分之一。为你的组件及其 props 编写文档并非严格必须。但这可能非常重要,取决于你的团队规模和应用规模。它非常简单,并且能立即见效。
This is one of the least essential but most important sections of this book. It’s not strictly necessary to document your components and their props. It may be very important, depending on the size of your team and your app. But it’s very easy, and bears immediate fruit.
要为组件及其 props 编写文档,你只需在组件函数和每一个 props 上添加文档注释即可:
To document a component and its props, you can simply add doc comments on the component function, and each one of the props:
/// 显示目标的进度。
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
/// 进度条的最大值。
/// The maximum value of the progress bar.
#[prop(default = 100)]
max: u16,
/// 应该显示的进度量。
/// How much progress should be displayed.
#[prop(into)]
progress: Signal<i32>,
) -> impl IntoView {
/* ... */
}
这就是你需要做的全部。这些注释的行为就像普通的 Rust 文档注释,不同之处在于你可以为单个组件 props 编写文档,而普通的 Rust 函数参数无法做到这一点。
That’s all you need to do. These behave like ordinary Rust doc comments, except that you can document individual component props, which can’t be done with Rust function arguments.
这将自动为你的组件、其 Props 类型以及用于添加 props 的每个字段生成文档。当你将鼠标悬停在组件名称或 props 上,并在此处看到 #[component] 宏与 rust-analyzer 结合的强大功能时,你可能才会意识到这有多么强大。
This will automatically generate documentation for your component, its Props type, and each of the fields used to add props. It can be a little hard to understand how powerful this is until you hover over the component name or props and see the power of the #[component] macro combined with rust-analyzer here.
将属性扩展到组件上
Spreading Attributes onto Components
有时你希望用户能够为组件添加额外的属性。例如,你可能希望用户能够添加自己的 class 或 id 属性,用于样式设置或其他目的。
Sometimes you want users to be able to add additional attributes to a component. For example, you might want users to be able to add their own class or id attributes for styling or other purposes.
你可以通过创建 class 或 id props 并在随后将其应用于适当的元素来实现这一点。但 Leptos 还支持将额外的属性 “扩展(spreading)” 到组件上。添加到组件的属性将应用于其视图返回的所有顶级 HTML 元素。
You could do this by creating class or id props that you then apply to the appropriate element. But Leptos also supports “spreading” additional attributes onto components. Attributes added to a component will be applied to all top-level HTML elements returned from its view.
// 你可以使用 view 宏并以扩展 {..} 作为标签名来创建属性列表
// you can create attribute lists by using the view macro with a spread {..} as the tag name
let spread_onto_component = view! {
<{..} aria-label="一个使用属性扩展的组件"/>
};
view! {
// 扩展到组件上的属性将应用于组件视图返回的 *所有* 元素。
// attributes that are spread onto a component will be applied to *all* elements returned as part of
// 要将属性应用于组件的一部分,请通过组件 prop 传递它们
// the component's view. to apply attributes to a subset of the component, pass them via a component prop
<ComponentThatTakesSpread
// 普通标识符用于 props
// plain identifiers are for props
some_prop="foo"
another_prop=42
// class:, style:, prop:, on: 语法的工作方式与它们在元素上完全相同
// the class:, style:, prop:, on: syntaxes work just as they do on elements
class:foo=true
style:font-weight="bold"
prop:cool=42
on:click=move |_| alert("点击了 ComponentThatTakesSpread")
// 要传递普通 HTML 属性,请使用 attr: 前缀
// to pass a plain HTML attribute, prefix it with attr:
attr:id="foo"
// 或者,如果你想包含多个属性,而不是为每个属性都加上 attr: 前缀,
// or, if you want to include multiple attributes, rather than prefixing each with
// 你可以使用扩展 {..} 将它们与组件 props 分开
// attr:, you can separate them from component props with the spread {..}
{..} // 此后的所有内容都被视为 HTML 属性
title="哦,一个标题!"
// 我们可以添加上面定义的整个属性列表
// we can add the whole list of attributes defined above
{..spread_onto_component}
/>
}
如果你想将属性提取到一个函数中,以便在多个组件中使用,你可以通过实现一个返回 impl Attribute 的函数来做到这一点。
这将使上面的示例看起来像这样:
fn spread_onto_component() -> impl Attribute {
view!{
<{..} aria-label="一个使用属性扩展的组件"/>
}
}
view!{
<SomeComponent {..spread_onto_component()} />
}
</div>
</div>
如果你想将属性扩展到组件上,但又想将属性应用到除所有顶级元素以外的其他元素上,请使用 AttributeInterceptor。
If you want to spread attributes onto a component, but want to apply the attributes to something other than all top-level elements, use AttributeInterceptor.
有关更多示例,请参阅 spread 示例。
See the spread example for more examples.
[点击打开 CodeSandbox。](https://codesandbox.io/p/devbox/3-components-0-7-rkjn3j?file=%2Fsrc%2Fmain.rs%3A39%2C10)
<noscript>
请启用 JavaScript 以查看示例。
Please enable JavaScript to view examples.
</noscript>
<template>
<iframe src="https://codesandbox.io/p/devbox/3-components-0-7-rkjn3j?file=%2Fsrc%2Fmain.rs%3A39%2C10" width="100%" height="1000px" style="max-height: 100vh"></iframe>
</template>
CodeSandbox Source
use leptos::prelude::*;
// 将不同的组件组合在一起是我们构建用户界面的方式。
// Composing different components together is how we build
// 在这里,我们将定义一个可复用的 <ProgressBar/>。
// user interfaces. Here, we'll define a reusable <ProgressBar/>.
// 你将看到如何使用文档注释来为组件及其属性编写文档。
// You'll see how doc comments can be used to document components
// and their properties.
/// 显示目标的进度。
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
// 将此标记为可选 prop。它将默认为其类型的默认值,
// Marks this as an optional prop. It will default to the default
// 即 0。
// value of its type, i.e., 0.
#[prop(default = 100)]
/// 进度条的最大值。
/// The maximum value of the progress bar.
max: u16,
// 将对传入 prop 的值运行 `.into()`。
// Will run `.into()` on the value passed into the prop.
#[prop(into)]
// `Signal<T>` 是几种响应式类型的包装器。
// `Signal<T>` is a wrapper for several reactive types.
// 在这样的组件 API 中它很有帮助,因为我们
// It can be helpful in component APIs like this, where we
// 可能想接收任何类型的响应式值
// might want to take any kind of reactive value
/// 应该显示的进度量。
/// How much progress should be displayed.
progress: Signal<i32>,
) -> impl IntoView {
view! {
<progress
max={max}
value=progress
/>
<br/>
}
}
#[component]
fn App() -> impl IntoView {
let (count, set_count) = signal(0);
let double_count = move || count.get() * 2;
view! {
<button
on:click=move |_| {
*set_count.write() += 1;
}
>
"Click me"
</button>
<br/>
// 如果你在 CodeSandbox 或具有 rust-analyzer 支持的编辑器中打开它,
// If you have this open in CodeSandbox or an editor with
// 请尝试将鼠标悬停在 `ProgressBar`、`max` 或 `progress` 上,
// rust-analyzer support, try hovering over `ProgressBar`,
// 以查看我们上面定义的文档
// `max` or `progress` to see the docs we defined above
<ProgressBar max=50 progress=count/>
// 在这一个上使用默认的 max 值
// Let's use the default max value on this one
// 默认值是 100,所以它的移动速度应该是原来的一半
// the default is 100, so it should move half as fast
<ProgressBar progress=count/>
// Signal::derive 从我们的派生信号中创建一个 Signal 包装器
// Signal::derive creates a Signal wrapper from our derived signal
// 使用 double_count 意味着它的移动速度应该是原来的两倍
// using double_count means it should move twice as fast
<ProgressBar max=50 progress=Signal::derive(double_count)/>
}
}
fn main() {
leptos::mount::mount_to_body(App)
}