Skip to content

插槽

插槽内容与出口

组件能够接收任意类型的JavaScript值作为props,也可以接收一些模板片段,在自身的组件中进行渲染.传递的模板片段就是插槽的内容,而组件内部渲染的位置就是插槽的出口.

html
<FancyButton>
  <!-- 插槽内容 -->
  Click me!
</FancyButton>
html
<!-- FancyButton 组件的模板 -->
<button class="fancy-btn">
  <slot></slot>
</button>

<solt>元素时一个插槽出口(slot outlet),表示了父元素提供的插槽内容将在哪里渲染

img.png

最终渲染结果为:

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

通过使用插槽,<FancyButton>仅负责渲染外层的<button>元素,而插槽内容Click me!则由父组件提供. 可以类比下面的代码来理解其概念

javascript
// 父元素传入插槽内容
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>

渲染作用域

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

html
<span>{{ message }}</span>

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

插槽内容无法访问子组件的数据.Vue模板中的表达式只能访问其定义时所处的作用域,也就是说:父组件模板中的表达式只能访问父组件的作用域; 子组件模板中的表达式只能访问子组件的作用域.

默认内容

在外部没有指定时,可以为插槽指定默认内容

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

默认内容会在插槽被显示提供内容时被覆盖

具名插槽

一个组件中,可以包含多个插槽出口.

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

对于这种场景,<slot>元素需要使用name属性来定义插槽的名称,为插槽分配唯一的ID,确定每一处要渲染的内容. 这样的插槽被成为具名插槽

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

在父组件中使用v-slot指令来指定插槽内容应该渲染到哪个插槽出口中,也可以简写为#,,插槽内容应该放在<template>元素中,<template>元素是Vue模板中的虚拟元素,不会渲染到页面上.

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

img.png

当组件同时接收默认插槽和具名插槽时,所有位于顶级的非<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>

条件插槽

插槽定义以后就会被渲染在最终的页面上,但有时候我们需要根据内容是否被传入了插槽来决定是否渲染,这时候可以结合$slots对象与v-if指令来实现

html
<template v-if="$slots.header">
  <header>
    <slot name="header"></slot>
  </header>
</template>

动态插槽名

动态指令参数在v-slot上也是有效的,因此可以用来定义动态的插槽名

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

作用域插槽

上面在渲染作用域中我们说,插槽的内容无法访问到子组件的状态.但是如果插槽的内容想要同时使用父组件和子组件域内的数据,可以像对组件传递props一样想插槽的出口上传递 attribute

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

默认插槽传递数据

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

这里默认插槽传递了textcount属性,父组件中在使用这个具有插槽的子组件时,通过v-slot指令的值来接收子组件传递的属性,v-slot的值是一个对象,包含了子组件传递的属性

img.png

这里可以借助父组件向子组件传值来理解,只不过这次数据的方向是反的,是插槽绑定的值传递到了父组件,提供给父组件使用

具名插槽传递数据

插槽props可以作为v-slot指令的值被访问到:v-slot:slotName="slotProps"

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

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

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

向具名插槽中传入props

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

需要注意的是,name是一个Vue特别保留的 attribute,如果使用name作为插槽的props名称,将会导致意料之外的行为,所以建议使用其他名称

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

html
<!-- <MyComponent> template -->
<div>
  <slot :message="hello"></slot>
  <slot name="footer" />
</div>

错误示例:

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

正确示例:

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

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

高级列表组件示例

这里有一个列表组件:

html
// FancyList.vue
<script setup>
  import { ref, onMounted } from 'vue'

  const props = defineProps({
    apiUrl: String,
    perPage: Number,
  })

  const items = ref([])
  const currentPage = ref(1)

  const fetchData = async () => {
    const res = await fetch(`${props.apiUrl}?page=${currentPage.value}&per_page=${props.perPage}`)
    items.value = await res.json()
  }

  onMounted(fetchData)
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      <!-- 将整个item对象作为插槽prop向下传递 -->
      <slot name="item" v-bind="item"></slot>
    </li>
  </ul>
</template>

在父组件中使用这个列表组件

html
<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组件,而列表组件将Item的渲染通过插槽暴露给父组件决定

无渲染组件

上面的<FancyList>组件封装了一些可复用的逻辑(数据获取,分页)和视图结构输出,但是夜间部分视图输出(Item项的渲染)交给了消费者组件来处理

扩展一下,如果有一个组件只包含逻辑而不渲染内容,视图输出完全交给消费者组件来处理,这样的组件成为无渲染组件