插槽内容与出口

组件能够接收任意类型的Javascript值作为props,但组件要如何接收模板内容?想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

举例:这里有一个<FancyButton>组件,可以像这样使用:

<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>

<FancyButton>的模板是这样的:

<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>

<slot>元素是一个插槽出口(slot outlet),标示了父元素提供的插槽内容(slot content)将在哪里被渲染。

最终渲染出的DOM是这样:

<button class="fancy-btn">Click me!</button>

通过使用插槽,<FancyButton>仅负责渲染外层的<button>(以及相应的样式),而其内部的内容由父组件提供。

理解插槽的另一种方式是和下面的Javascript函数作类比,其概念是类似的:

//父元素传入插槽内容
FancyButton('Click me!')

//FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) {
return `button class="fancy-btn">
${slotContent}
</button>`
}

插槽内容可以是任意合法的模板内容,不局限于文本。可以传入多个元素,甚至是组件:

<FancyButton>
<span style="color: red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>

通过使用插槽,<FancyButton>组件更加灵活和具有可复用性,现在组件可以用在不同的地方渲染各异的内容,但同时还保证都具有相同的样式。

渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例说:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

这两个{{ message }}插值表达式渲染的内容都是一样的。

插槽内容无法访问子组件的数据。Vue模板中的表达式只能访问其定义时所处的作用域,这和Javascript的词法作用域规则是一致的。

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。如有这样一个<SubmitButton>组件:

<button type="submit">
<slot></slot>
</button>

如果想在父组件没有提供任何插槽内容时在<button>内渲染”Submit”,只需要将”Submit”写在<slot>标签之间来作为默认内容:

<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>

现在当在父组件中使用<SubmitButton>且没有提供任何插槽内容时:

<SubmitButton />

“Submit”将会被作为默认内容渲染:

<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>元素可以有一个特殊的attributename,用来给各个插槽分配唯一的ID,以确定每一处要渲染的内容:

<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

这类带name的插槽被称为具名插槽(named slots)。没有提供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>节点都被隐式地视为默认插槽的内容,所以上面的内容可以改写为:

<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>

<!-- 隐式的默认插槽 -->
<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如下:

<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>

条件插槽

有时候需要根据内容是否被传入了插槽来渲染某些内容。可以结合使用$slots属性和v-if来实现。

下面的示例中,定义一个卡片组件,它拥有三个条件插槽:headerfooterdefault。当header、footer或default的内容存在时,可以包装提供额外的样式:

<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上也是有效的,即可以定义下面这样的动态插槽名:

<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>

<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>

作用域插槽

前面提到插槽的内容无法访问到子组件的状态。

然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,可以让子组件在渲染的时候将一部分数据提供给插槽。
像对组件传递props那样,向一个插槽的出口上传递attributes:

<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>

当需要接收插槽props时,默认插槽和具名插槽的使用方式有一些小区别。

  1. 默认插槽接收props,通过子组件标签上的v-slot指令,直接接收到了一个插槽props对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

子组件传入插槽的props作为了v-slot指令的值,可以在插槽内的表达式中访问。

v-slot="slotProps"与函数的参数类似,可以在v-slot中使用解构:

<MyComponent v-slot="{text, count}">
{{ text }} {{ count }}
</MyComponent>
  1. 具名作用域插槽,插槽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指令将导致编译错误。

为默认插槽使用显式的<template>标签有助于更清晰地指出message属性在其他插槽中不可用:

<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>

<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>