组件和属性(Props)
到目前为止,我们一直在单个组件中构建整个应用程序。对于非常小的示例来说,这种方式完全没问题,但在任何实际应用中,你都需要将用户界面拆分成多个组件,以便将界面分解为更小、可复用、可组合的部分。
让我们以进度条为例。假设你希望有两个进度条,而不是一个:一个每次点击前进一个刻度,另一个每次点击前进两个刻度。
你可以通过简单地创建两个 <progress> 元素来实现:
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
/>
}
但显然,这种方法并不具有很好的扩展性。如果你想添加第三个进度条,就需要再添加一份代码。而如果你想修改任何与进度条相关的内容,就需要在三个地方分别进行修改。
因此,我们可以创建一个 <ProgressBar/> 组件来解决这个问题。
#[component]
fn ProgressBar() -> impl IntoView {
view! {
<progress
max="50"
// 但是……这应该从哪里得到呢?
value=progress
/>
}
}
现在有一个问题:progress 尚未定义。那么它应该从哪里来呢?当我们手动定义所有内容时,我们直接使用了局部变量名。但现在,我们需要一种方法将参数传递给组件。
组件属性(Props)
我们通过组件属性(properties或称“props”)来实现这一点。如果你使用过其他前端框架,这个概念应该很熟悉。本质上,属性对于组件的作用,就像属性对于 HTML 元素的作用一样:它们允许你向组件传递额外的信息。
在 Leptos 中,可以通过为组件函数添加额外的参数来定义 props。
#[component]
fn ProgressBar(
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
<progress
max="50"
// 现在便正常工作了
value=progress
/>
}
}
现在我们可以在主要的 <App/> 组件的视图中使用我们的组件。
#[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 属性,非常简单。
响应式与静态 Props
在这个示例中,你会注意到 progress 接受的是一个响应式的 ReadSignal<i32>,而不是一个普通的 i32。这一点 非常重要。
组件的 props 并没有附加任何特殊意义。组件本质上只是一个函数,用来初始化用户界面。而要让界面对变化做出响应,唯一的方法就是传递一个信号类型。因此,如果你的组件属性会随着时间变化,比如我们的 progress,它就应该是一个信号。
optional Props
目前,max 设置是硬编码的。我们也可以将它作为一个 prop 来传递。但是让我们将这个 prop 设置为可选。我们可以通过用 #[prop(optional)] 标注它来实现这一点。
#[component]
fn ProgressBar(
// 将这个 prop 标注为可选
// 使用 `<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。但对于进度条来说,max 为 0 的值并不太有用。
因此,我们可以为它设置一个特定的默认值。
default props
您可以简单地通过定义 #[prop(default = ...) 来指定一个默认值,而不是使用 Default::default() 自动确定。
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
<progress
max=max
value=progress
/>
}
}
泛型属性(Generic Props)
这是很棒的例子。但一开始我们使用了两个计数器,一个由 count 驱动,另一个由派生信号 double_count 驱动。让我们通过将 double_count 用作另一个 <ProgressBar/> 的 progress 属性来重新创建它。
#[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/>
// 添加第二个进度条
<ProgressBar progress=double_count/>
}
}
嗯……这个代码无法编译。这很容易理解原因:我们声明了 progress 属性接受 ReadSignal<i32> 类型,而 double_count 不是 ReadSignal<i32> 类型。正如 rust-analyzer 所提示的,它的类型是 || -> i32,即返回 i32 的闭包。
有几种方法可以解决这个问题。我们可以这样说:“嗯,我知道视图要实现响应式,需要接受一个函数或信号。我可以通过将信号包装成闭包来将信号转换成函数……也许我可以直接接受任何函数?”如果你很熟悉,你可能知道这些都实现了 Fn() -> i32 trait。因此,可以使用泛型组件:
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
progress: impl Fn() -> i32 + Send + Sync + 'static
) -> impl IntoView {
view! {
<progress
max=max
value=progress
/>
// 添加换行符以避免重叠
<br/>
}
}
这样写是完全合理的:progress 属性现在可以接受任何实现了 Fn() trait 的值。
泛型属性也可以通过
where子句指定,或者使用内联泛型,比如ProgressBar<F: Fn() -> i32 + 'static>。
泛型需要在组件的属性中某处使用。这是因为属性会被构建成一个结构体,因此所有泛型类型都必须在结构体中使用。这通常可以通过可选的 PhantomData 属性轻松实现。然后可以使用表示类型的语法 <Component<T>/>(而不是 turbofish 风格 <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>>/>。
into 属性
还有一种方法可以实现上述功能,就是使用 #[prop(into)]。
这个属性会自动对传递给属性的值调用 .into() 方法,从而让你轻松传递不同类型的值作为属性。
在这种情况下,了解 Signal 类型会很有帮助。
Signal 是一种枚举类型,表示任何可读的响应式信号或普通值。在定义组件 API 时,如果希望组件能够接受不同类型的信号,Signal 会非常有用。
#[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`
<ProgressBar progress=count/>
// 使用 `Signal::derive()` 包装派生信号
<ProgressBar progress=Signal::derive(double_count)/>
}
}
Optional Generic Props
注意,您无法为组件指定可选的泛型属性。让我们看看如果尝试这样做会发生什么:
#[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 会给出下面有用的报错:
xx | <ProgressBar/>
| ^^^^^^^^^^^ 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)。然而,在这里指定正确的类型是不可能的,因为闭包和函数通常是无法命名的类型。编译器可能会用一种简写显示它们,但您无法直接指定它们。
不过,您可以通过使用 Box<dyn _> 或 &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 编译器现在知道属性的具体类型,因此即使在 None 的情况下,它也知道内存中的大小,这样就可以正常编译了。
在本例中,
&dyn Fn() -> i32会引发生命周期问题,但在其他情况下,它可能是一个可行的选择。
为组件编写文档
这是本书中最非必要却又最重要的部分之一。
记录组件及其属性并非严格必要,但根据团队规模和应用程序的复杂性,这可能变得非常重要。而且,这很简单,并且可以立即见效。
要为组件及其属性编写文档,您只需为组件函数和每个属性添加文档注释即可:
/// 显示目标的进度。
#[component]
fn ProgressBar(
/// 进度条的最大值。
#[prop(default = 100)]
max: u16,
/// 显示的进度值。
#[prop(into)]
progress: Signal<i32>,
) -> impl IntoView {
/* ... */
}
这就是您需要做的全部。这些注释的行为与普通的 Rust 文档注释相同,只不过您可以为单个组件的属性编写文档,而这在普通的 Rust 函数参数中是无法做到的。
这将自动生成组件的文档,包括其 Props 类型及用于添加属性的每个字段的文档。
直到您在组件名称或属性上悬停鼠标,看到 #[component] 宏结合 rust-analyzer 的强大功能,您才会真正理解这一点的强大之处。
将属性扩展到组件
有时候,您希望用户能够向组件添加额外的属性,例如,允许用户添加自己的 class 或 id 属性以便进行样式设计或其他目的。
一种方法是为组件创建 class 或 id 属性,并将它们应用到适当的元素。然而,Leptos 还支持将额外的属性“扩展”到组件上。添加到组件的属性将应用到其视图中返回的所有顶级 HTML 元素。
// 您可以使用 view 宏通过扩展 {..} 创建属性列表
let spread_onto_component = view! {
<{..} aria-label="a component with attribute spreading"/>
};
view! {
// 扩展到组件的属性将应用于组件视图中返回的 *所有* 元素。
// 要将属性应用于组件的一部分,请通过组件属性传递它们
<ComponentThatTakesSpread
// 普通标识符用于组件属性
some_prop="foo"
another_prop=42
// class:, style:, prop:, on: 语法与在 HTML 元素上使用时完全相同
class:foo=true
style:font-weight="bold"
prop:cool=42
on:click=move |_| alert("clicked ComponentThatTakesSpread")
// 要传递普通的 HTML 属性,请加上 attr: 前缀
attr:id="foo"
// 或者,如果您想包含多个属性,而不是为每个属性添加 attr: 前缀,
// 可以使用扩展 {..} 将它们与组件属性分开
{..} // 此后的内容将被视为 HTML 属性
title="ooh, a title!"
// 我们可以添加上面定义的整个属性列表
{..spread_onto_component}
/>
}
更多示例请参见 spread 示例。
CodeSandbox Source
use leptos::prelude::*;
// Composing different components together is how we build
// 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(
// Marks this as an optional prop. It will default to the default
// value of its type, i.e., 0.
#[prop(default = 100)]
/// The maximum value of the progress bar.
max: u16,
// Will run `.into()` on the value passed into the prop.
#[prop(into)]
// `Signal<T>` is a wrapper for several reactive types.
// 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/>
// If you have this open in CodeSandbox or an editor with
// rust-analyzer support, try hovering over `ProgressBar`,
// `max`, or `progress` to see the docs we defined above
<ProgressBar max=50 progress=count/>
// Let's use the default max value on this one
// the default is 100, so it should move half as fast
<ProgressBar progress=count/>
// Signal::derive creates a Signal wrapper from our derived signal
// 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)
}