插槽
本页面假设您已经阅读了组件基础知识。如果您对组件是新手,请首先阅读。
插槽内容和出口
我们已经了解到组件可以接受属性,这些属性可以是任何类型的JavaScript值。但是模板内容怎么办?在某些情况下,我们可能想要将一个模板片段传递给子组件,并让子组件在其自己的模板内渲染该片段。
例如,我们可能有一个支持以下使用的<FancyButton>
组件
template
<FancyButton>
Click me! <!-- slot content -->
</FancyButton>
<FancyButton>
的模板看起来像这样
template
<button class="fancy-btn">
<slot></slot> <!-- slot outlet -->
</button>
<slot>
元素是一个插槽出口
,它指示父组件提供的插槽内容
应渲染的位置。
并且最终的渲染DOM
html
<button class="fancy-btn">Click me!</button>
使用插槽,<FancyButton>
负责渲染外部的<button>
(及其华丽的样式),而内部内容由父组件提供。
另一种理解插槽的方式是通过将其与JavaScript函数进行比较
js
// parent component passing slot content
FancyButton('Click me!')
// FancyButton renders slot content in its own template
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}
插槽内容不仅限于文本。它可以是以任何有效模板内容。例如,我们可以传递多个元素,甚至其他组件
template
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
通过使用插槽,我们的<FancyButton>
变得更加灵活和可重用。现在我们可以在不同的位置使用它,并且具有不同的内部内容,但所有这些都具有相同的时尚样式。
Vue组件的插槽机制灵感来源于原生Web组件<slot>
元素,但它具有我们稍后将要看到的附加功能。
渲染范围
插槽内容可以访问父组件的数据作用域,因为它是在父组件中定义的。例如
template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
这里两个{{ message }}
插值将渲染相同的内容。
插槽内容无法访问子组件的数据。Vue模板中的表达式只能访问其定义的作用域,这与JavaScript的词法作用域一致。换句话说
父模板中的表达式仅能访问父作用域;子模板中的表达式仅能访问子作用域。
备用内容
有时为插槽指定备用(即默认)内容非常有用,以便在没有提供内容时进行渲染。例如,在<SubmitButton>
组件中
template
<button type="submit">
<slot></slot>
</button>
我们可能希望文本“提交”在父组件没有提供任何插槽内容的情况下渲染在<button>
内。要将“提交”作为备用内容,我们可以将其放置在<slot>
标签之间
template
<button type="submit">
<slot>
Submit <!-- fallback content -->
</slot>
</button>
现在,当我们在父组件中使用<SubmitButton>
,没有为插槽提供内容时
template
<SubmitButton />
这将渲染备用内容,“提交”
html
<button type="submit">Submit</button>
但如果我们提供内容
template
<SubmitButton>Save</SubmitButton>
那么提供的内容将被渲染
html
<button type="submit">Save</button>
命名插槽
有时在单个组件中具有多个插槽出口很有用。例如,在具有以下模板的<BaseLayout>
组件中
template
<div class="container">
<header>
<!-- We want header content here -->
</header>
<main>
<!-- We want main content here -->
</main>
<footer>
<!-- We want footer content here -->
</footer>
</div>
在这些情况下,<slot>
元素具有一个特殊属性name
,可以用来为不同的插槽分配一个唯一的ID,以便确定内容应该渲染的位置
template
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
没有name
的<slot>
出口隐式具有“默认”名称。
在父组件中使用<BaseLayout>
时,我们需要一种方法来传递多个插槽内容片段,每个片段针对不同的插槽出口。这就是命名插槽的作用。
要传递命名插槽,我们需要使用带有v-slot
指令的<template>
元素,然后传递插槽名称作为v-slot
的参数
template
<BaseLayout>
<template v-slot:header>
<!-- content for the header slot -->
</template>
</BaseLayout>
v-slot
有一个专门的简写#
,所以<template v-slot:header>
可以缩短为<template #header>
。把它想象成“在子组件的‘header’插槽中渲染此模板片段”。
以下是使用简写语法将内容传递给所有三个插槽的代码
template
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
当一个组件接受默认插槽和命名插槽时,所有顶级非<template>
节点都将隐式地视为默认插槽的内容。因此,上面的代码也可以写成
template
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- implicit default slot -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
现在,所有在<template>
元素内部的内容都将传递给相应的插槽。最终渲染的HTML将是
html
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
再次,使用JavaScript函数的类比可能有助于您更好地理解命名插槽
js
// passing multiple slot fragments with different names
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout> renders them in different places
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}
条件插槽
有时,您可能需要根据插槽是否存在来渲染某些内容。
您可以使用 $slots 属性与 v-if 结合使用来实现这一点。
以下示例中,我们定义了一个具有三个条件插槽的 Card 组件:header
、footer
和 default
。当存在头部/尾部/默认插槽时,我们想要将它们包裹起来以提供额外的样式。
template
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div v-if="$slots.default" class="card-content">
<slot />
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
动态插槽名称
动态指令参数 也可以用于 v-slot
,允许定义动态插槽名称。
template
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- with shorthand -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
请注意,表达式受到 动态指令参数语法约束 的限制。
作用域插槽
如 渲染作用域 所述,插槽内容无法访问子组件的状态。
然而,在某些情况下,如果插槽的内容可以同时使用父作用域和子作用域中的数据,那么这可能很有用。为了实现这一点,我们需要一种方法,让子组件在渲染时将数据传递给插槽。
实际上,我们可以这样做——我们可以像传递 prop 给组件一样将属性传递给插槽出口。
template
<!-- <MyComponent> template -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
在使用单个默认插槽与使用命名插槽接收 prop 时有所不同。我们将首先通过在子组件标签上直接使用 v-slot
来展示如何使用单个默认插槽接收 prop。
template
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
子组件传递给插槽的 prop 可以作为相应 v-slot
指令的值使用,可以在插槽内的表达式中访问。
您可以将作用域插槽视为传递给子组件的函数。然后子组件调用它,并将 prop 作为参数传递。
js
MyComponent({
// passing the default slot, but as a function
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// call the slot function with props!
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}
实际上,这与作用域插槽的编译方式非常接近,以及您如何在使用手动 渲染函数 时使用作用域插槽。
注意 v-slot="slotProps"
与插槽函数签名的匹配。就像函数参数一样,我们可以在 v-slot
中使用解构。
template
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
命名作用域插槽
命名作用域插槽的工作方式类似——插槽 prop 可以作为 v-slot
指令的值访问:v-slot:name="slotProps"
。当使用简写时,它看起来像这样
template
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
将 prop 传递给命名插槽
template
<slot name="header" message="hello"></slot>
请注意,插槽的 name
不会包含在 prop 中,因为它被保留——所以生成的 headerProps
将是 { message: 'hello' }
。
如果您正在将命名插槽与默认作用域插槽混合使用,则需要为默认插槽使用显式的 <template>
标签。直接在组件上放置 v-slot
指令会导致编译错误。这是为了避免对默认插槽的 prop 作用域的任何歧义。例如
template
<!-- <MyComponent> template -->
<div>
<slot :message="hello"></slot>
<slot name="footer" />
</div>
template
<!-- This template won't compile -->
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message belongs to the default slot, and is not available here -->
<p>{{ message }}</p>
</template>
</MyComponent>
使用显式的 <template>
标签为默认插槽有助于明确 message
prop 不会在其它插槽内部可用。
template
<MyComponent>
<!-- Use explicit default slot -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
复杂列表示例
您可能想知道范围插槽有什么好的用例。以下是一个示例:想象一个 <FancyList>
组件,它渲染一个项目列表 - 它可以封装加载远程数据的逻辑、使用数据显示列表,甚至高级功能如分页或无限滚动。然而,我们希望它对每个项目的样式灵活,将每个项目的样式留给父组件来处理。所以期望的使用方式可能看起来像这样
template
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
在 <FancyList>
内部,我们可以多次渲染相同的 <slot>
,并使用不同的项目数据(注意我们使用 v-bind
将对象作为插槽属性传递)
template
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
无渲染组件
我们上面讨论的 <FancyList>
用例封装了可重用逻辑(数据获取、分页等)和视觉输出,并通过范围插槽将部分视觉输出委托给消费者组件。
如果我们进一步推进这个概念,我们可以得到只封装逻辑而不自行渲染的组件 - 视觉输出完全通过范围插槽委托给消费者组件。我们称这种类型的组件为 无渲染组件。
一个示例无渲染组件可能是封装跟踪当前鼠标位置逻辑的组件
template
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
虽然这是一个有趣的模式,但使用组合式API可以以更高效的方式实现大多数使用无渲染组件可以完成的事情,而不必承担额外的组件嵌套的开销。稍后我们将看到如何将相同的鼠标跟踪功能实现为一个 可组合的组件。
话虽如此,范围插槽在需要同时封装逻辑 和 组合视觉输出的情况下仍然很有用,例如在 <FancyList>
示例中。