目录
一、插槽内容与出口
二、渲染作用域
三、默认内容
四、具名插槽
五、作用域插槽
六、具名作用域插槽
一、插槽内容与出口
在之前的博文中,我们已经了解到组件能够接收任意类型的JS值作为props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
举例来说,这里有一个<FancyButton>组件,可以像这样使用:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而<FancyButton>模板是这样的:
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染的结果是这样的:
<button class="fancy-btn">Click me!</button>
通过使用插槽,<FancyButton>
仅负责渲染外层的 <button>
(以及相应的样式),而其内部的内容由父组件提供。通过使用插槽,<FancyButton>
组件更加灵活和具有可复用性。现在组件可以用在不同的地方渲染各异的内容,但同时还保证都具有相同的样式。
二、渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
这里的两个 {{ message }}
插值表达式渲染的内容都是一样的。
插槽内容无法访问子组件的数据。Vue模板中的表达式只能访问其定义时所处的作用域,这和JS的词法作用域规则是一致的。
三、默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个<SubmitButton>组件:
<button type="submit">
<slot></slot>
</button>
如果我们想在父组件没有提供任何插槽内容时在<button>内渲染“Submit”,只需要将“Submit”写在<slot>标签之间来作为默认内容:
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
现在,当我们在父组件中使用 <SubmitButton>
且没有提供任何插槽内容时,“Submit”也会被默认渲染:
父组件:
<SubmitButton />
子组件:
<button type="submit">Submit</button>
但如果我们提供了插槽内容,那么被显式提供的内容会取代默认内容:
父组件:
<SubmitButton>Save</SubmitButton>
子组件:
<button type="submit">Save</button>
四、具名插槽
有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个<BaseLayout>组件中,有如下模板:
<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>
对于这种场景,<slot>元素可以有一个特殊的attribute name,用来给各个插槽分配唯一的ID,以确定每一处要渲染的内容:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带name的插槽被称为具名插槽。没有提供name的<slot>出口会隐匿地命名为"default"。
在父组件中使用<BaseLayout>时,我们需要一种方式将多个插槽内容人传入到各自目标插槽的出口。此时就需要用到具名插槽了:
要为具名插槽传入内容,我们需要使用一个含v-slot指令的<template>元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot
有对应的简写 #
,因此 <template v-slot:header>
可以简写为 <template #header>
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
举例:
<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>元素中的所有内容都将被传递到相应的插槽。最终渲染出的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>
五、作用域插槽
在上面的渲染作用域中我们讨论到,插槽的内容无法访问到子组件的状态。然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。
我们也确实有办法这么做!可以像对组件传递props那样,向一个插槽的出口上传递attributes:
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
当需要接收插槽props时,默认插槽和具名插槽的使用方式有一些小区别。下面我们将先展示默认插槽如何接受props,通过子组件标签上的v-slot指令,直接接收到了一个插槽props对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
六、具名作用域插槽
具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot
指令的值被访问到:v-slot:name="slotProps"
。当使用缩写时是这样:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
向具名插槽中传入props:
<slot name="header" message="hello"></slot>
注意插槽上的 name
是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps
的结果是 { message: 'hello' }
。
如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。尝试直接为组件添加 v-slot
指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。
你可能想问什么样的场景才适合用到作用域插槽,这里我们来看一个 <FancyList>
组件的例子。它会渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。我们期望的用法可能是这样的:
<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
来传递插槽的 props):
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>