Skip to content

插槽 Slots

插槽内容与出口

在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

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

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

<FancyButton> 的模板是这样的:

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

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

插槽图示

最终渲染出的 DOM 是这样:

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

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

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

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

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

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

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

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

Vue 组件的插槽机制是受原生 Web Component slot 元素 的启发而诞生,同时还做了一些功能拓展,这些拓展的功能我们后面会学习到。

渲染作用域

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

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

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

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

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

默认内容

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

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

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

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

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

vue
<SubmitButton />

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

vue
<button type="submit">Submit</button>

但如果我们提供了插槽内容:

vue
<SubmitButton>Save</SubmitButton>

那么被显式提供的内容会取代默认内容:

vue
<button type="submit">Save</button>

具名插槽

有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout> 组件中,有如下模板:

html
<div class="container">
  <header>
    <!-- 标题内容放这里 -->
  </header>
  <main>
    <!-- 主要内容放这里 -->
  </main>
  <footer>
    <!-- 底部内容放这里 -->
  </footer>
</div>

对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

html
<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> 元素,并将目标插槽的名字传给该指令:

html
<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

具名插槽图示

下面我们给出完整的、向 <BaseLayout> 传递插槽内容的代码,指令均使用的是缩写形式:

html
<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
<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 如下:

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
// 传入不同的内容给不同名字的插槽
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`,
});

// <BaseLayout> 渲染插槽内容到对应位置
function BaseLayout(slots) {
  return `
    <div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>
    `;
}

动态插槽名

动态指令参数v-slot 上也是有效的,即可以定义下面这样的动态插槽名:

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

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

注意这里的表达式和动态指令参数受相同的语法限制

作用域插槽

在上面的渲染作用域中我们讨论到,插槽的内容无法访问到子组件的状态。

然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。

我们也确实有办法这么做!可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:

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

当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。下面我们将先展示默认插槽如何接受 props,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:

vue
<template>
  <MyComponent v-slot="slotProps">
    {{ slotProps.text }} {{ slotProps.count }}
  </MyComponent>
</template>

scoped slots diagram

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

你可以将作用域插槽类比为一个传入子组件的函数。子组件会将相应的 props 作为参数传给它:

js
MyComponent({
  // 类比默认插槽,将其想成一个函数
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`;
  },
});

function MyComponent(slots) {
  const greetingMessage = "hello";
  return `<div>${
    // 在插槽函数调用时传入 props
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`;
}

实际上,这已经和作用域插槽的最终代码编译结果、以及手动编写渲染函数 时使用作用域插槽的方式非常类似了。

v-slot="slotProps" 可以类比这里的函数签名,和函数的参数类似,我们也可以在 v-slot 中使用解构:

vue
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>

具名作用域插槽

具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps"。当使用缩写时是这样:

html
<MyComponent>
  <template #header="headerProps"> {{ headerProps }} </template>

  <template #default="defaultProps"> {{ defaultProps }} </template>

  <template #footer="footerProps"> {{ footerProps }} </template>
</MyComponent>

向具名插槽中传入 props:

vue
<slot name="header" message="hello"></slot>

注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: 'hello' }

如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。尝试直接为组件添加 v-slot 指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。举例:

vue
<!-- 该模板无法编译 -->
<template>
  <MyComponent v-slot="{ message }">
    <p>{{ message }}</p>
    <template #footer>
      <!-- message 属于默认插槽,此处不可用 -->
      <p>{{ message }}</p>
    </template>
  </MyComponent>
</template>

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

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

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

案例

具名插槽

当一个组件内有 2 处以上需要外部传入标签的地方,传入的标签可以分别派发给不同的 slot 位置

要求:v-slot 一般用跟 template 标签使用 (template 是 html5 新出标签内容模板元素,不会渲染到页面上,一般被 vue 解析内部标签)

components/Poetry.vue - 留下具名 slot

vue
<template>
  <div>
    <div class="title">
      <slot name="title"></slot>
    </div>
    <div class="author">
      <slot name="author"></slot>
    </div>
    <div class="content">
      <slot name="content"></slot>
    </div>
  </div>
</template>

App.vue 使用

vue
<script setup>
import Pannel from "./components/Poetry.vue";
</script>

<template>
  <div id="main">
    <Poetry>
      <template #title> 静夜思 </template>
      <template #author> 李白 </template>
      <template #content>
        <p>床前明月光,疑是地上霜。</p>
        <p>举头望明月,低头思故乡。</p>
      </template>
    </Poetry>
  </div>
</template>

<style scoped>
#main {
  width: 400px;
  margin: 20px auto;
  background-color: #fff;
  border: 4px solid blueviolet;
  border-radius: 1em;
  box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
  padding: 1em 2em 2em;
}
</style>
  • v-bind 可以省略成 :
  • v-on: 可以省略成 @
  • v-slot 可以简化成 #

总结:slot 的 name 属性起插槽名,使用组件时,template 配合 # 插槽名传入具体标签

作用域插槽

目标:子组件里值,在给插槽赋值时在父组件环境下使用

复习:插槽内 slot 中显示默认内容

例子:默认内容在子组件中,但是父亲在给插槽传值,想要改变插槽显示的默认内容

口诀:

  1. 子组件,在 slot 上绑定属性和子组件内的值
  2. 使用组件,传入自定义标签,用 template 和 v-slot="自定义变量名"
  3. scope 变量名自动绑定 slot 上所有属性和值

components/Row.vue - 定义组件,和具名插槽,给 slot 绑定属性和值

vue
<script setup>
import { reactive, ref } from "vue";

// 目标:作用域插槽
// 场景:使用插槽,使用组件内的变量
// 1. slot 标签,自定义属性和内变量关联
// 2. 使用组件,template 配合 v-slot="变量名"
// 变量名会收集 slot 身上属性和值形成对象
const isShow = ref(false);
const data = reactive({
  name: "正心",
  age: 18,
});
</script>

<template>
  <slot :data="data">
    <tr>
      <td>{{ data.name }}</td>
      <td>{{ data.age }}</td>
    </tr>
  </slot>
</template>

views/05_UseSlot.vue

vue
<script setup>
import Row from "./components/Row.vue";
</script>

<template>
  <div id="table">
    <table>
      <Row>
        <!-- 需求:插槽时,使用组件内变量 -->
        <!-- scope 变量:{data: data} -->
        <template v-slot="scope">
          <tr>
            <td>scope.data.name</td>
            <td>scope.data.age</td>
            <td><a href="">删除</a><a href="">修改</a></td>
          </tr>
        </template>
      </Row>
    </table>
  </div>
</template>

总结:组件内变量绑定在 slot 上,然后使用组件 v-slot="变量" 变量上就会绑定 slot 身上属性和值