跳至主要內容

插槽

大约 9 分钟

https://cn.vuejs.org/guide/components/slots.html#slots

认识组件 Slot

  • 在开发中,我们会经常封装一个个可复用的组件:
    • 前面我们会通过props传递给组件一些数据,让组件来进行展示;
    • 但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素
    • 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片;
    • 我们应该让使用者可以决定某一块区域到底存放什么内容和元素
  • 举个栗子:假如我们定制一个通用的导航组件 - NavBar
    • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
    • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
    • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
    • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;
在这里插入图片描述
在这里插入图片描述

如何使用插槽slot?

  • 这个时候我们就可以来定义插槽slot:
    • 插槽的使用过程其实是抽取共性、预留不同;
    • 我们会将共同的元素、内容依然在组件内进行封装;
    • 同时会将不同的元素使用 slot 作为占位,让外部决定到底显示什么样的元素;
  • 如何使用slot呢?
    • Vue中将 元素作为承载分发内容的出口;
    • 在封装组件中,使用特殊的元素就可以为封装组件开启一个插槽;
    • 该插槽插入什么内容取决于父组件如何使用;

插槽的基本使用

<script>
import FancyButton from './FancyButton.vue'
  
export default {
  components: { FancyButton }
}
</script>

<template>
  <FancyButton>
    Click me <!-- slot content -->
 	</FancyButton>

  <FancyButton>
  </FancyButton>
</template>
<template>
  <button class="fancy-btn">
  	<slot> Hello </slot>
  </button>
</template>

<style>
.fancy-btn {
  color: #fff;
  background: linear-gradient(315deg, #42d392 25%, #647eff);
  border: none;
  padding: 5px 10px;
  margin: 5px;
  border-radius: 8px;
  cursor: pointer;
}
</style>

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

image-20231205010910410
image-20231205010910410

通过使用插槽,<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>

插槽的默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <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>

多个插槽的效果

<template>
  <div>
    <my-slot-cpn>
      <button>我是按钮</button>
    </my-slot-cpn>

    <my-slot-cpn>
      我是普通的文本
    </my-slot-cpn>

    <my-slot-cpn></my-slot-cpn>

    <my-slot-cpn>
      <h2>哈哈哈</h2>
      <button>我是按钮</button>
      <strong>我是strong</strong>
    </my-slot-cpn>
  </div>
</template>

<script>
import MySlotCpn from './MySlotCpn.vue';

export default {
  components: {
    MySlotCpn,
  }
}
</script>
<template>
  <div>
    <h2>组件开始</h2>
    <slot>
      <i>我是默认的i元素</i>
    </slot>
    <slot>
      <i>我是默认的i元素</i>
    </slot>
    <slot>
      <i>我是默认的i元素</i>
    </slot>
    <h2>组件结束</h2>
  </div>
</template>
image-20231205011717002
image-20231205011717002

具名插槽的使用

有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <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 的插槽被称为具名插槽 (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 插槽中”。

image-20231205012048085
image-20231205012048085
<script>
import BaseLayout from './BaseLayout.vue'
  
export default {
  components: {
    BaseLayout
  }
}
</script>

<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>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<style>
  footer {
    border-top: 1px solid #ccc;
    color: #666;
    font-size: 0.8em;
  }
</style>

使用 JavaScript 函数来类比可能更有助于你来理解具名插槽:

// 传入不同的内容给不同名字的插槽
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>`
}

动态插槽名

在Vue 3中,动态插槽名可以使用v-slot指令来实现。你可以将插槽名作为一个变量来传递给v-slot指令,以实现动态插槽名的效果。

例如,如果你有一个动态的插槽名变量slotName,你可以这样使用动态插槽名:

<template v-slot:[slotName]>
  <!-- 插槽内容 -->
</template>

这样,slotName变量的值将作为插槽名来动态指定插槽的位置。

image-20231205012902405
image-20231205012902405

渲染作用域

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

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

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

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

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

作用域插槽

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

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

<script>
export default {
  data() {
    return {
      greetingMessage: 'hello'
    }
  }
}
</script>

<template>
  <div>
  	<slot :text="greetingMessage" :count="1"></slot>
	</div>
</template>

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

<script>
import MyComponent from './MyComponent.vue'
  
export default {
  components: {
    MyComponent
  }
}
</script>

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

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

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

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

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

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

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

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

具名作用域插槽

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

<script>
import MyComponent from './MyComponent.vue'
  
export default {
  components: {
    MyComponent
  }
}
</script>

<template>
  <div>
    <MyComponent>
      <template #header="slotProps">
        <h2>{{ slotProps.title }}</h2>
      </template>
      <template #content="slotProps">
        <p>{{ slotProps.text }}</p>
      </template>
    </MyComponent>
  </div>
</template>
<template>
  <div>
    <slot name="header" :title="title"></slot>
    <slot name="content" :text="content"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: 'Hello',
      content: 'This is the content'
    };
  }
};
</script>

独占默认插槽的缩写

  • 如果我们的插槽是默认插槽default,那么在使用的时候 v-slot:default="slotProps"可以简写为v-slot=“slotProps”
在这里插入图片描述
在这里插入图片描述
  • 并且如果我们的插槽只有默认插槽时,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直接用在组件上
在这里插入图片描述
在这里插入图片描述

默认插槽和具名插槽混合

  • 但是,如果我们有默认插槽和具名插槽,那么按照完整的template来编写。
在这里插入图片描述
在这里插入图片描述
  • 只要出现多个插槽,请始终为所有的插槽使用完整的基于
在这里插入图片描述
在这里插入图片描述