文章目录
- 1. 插槽简介
- 1.1 什么是插槽?
- 1.2 为什么需要插槽?
- 1.3 插槽的基本语法
- 2. 默认插槽
- 2.1 什么是默认插槽?
- 2.2 默认插槽语法
- 2.3 插槽默认内容
- 2.4 默认插槽实例:创建一个卡片组件
- 2.5 Vue 3中的默认插槽
- 2.6 默认插槽的应用场景
- 3. 具名插槽
- 3.1 什么是具名插槽?
- 3.2 具名插槽语法
- 3.3 具名插槽缩写
- 3.4 默认插槽的显式名称
- 3.5 具名插槽实例:页面布局组件
- 3.6 Vue 3中的具名插槽
- 3.7 具名插槽的应用场景
- 3.8 动态插槽名
- 4. 作用域插槽
- 4.1 什么是作用域插槽?
- 4.2 为什么需要作用域插槽?
- 4.3 作用域插槽语法
- 4.4 解构插槽Props
- 4.5 作用域插槽实例:自定义列表渲染
- 4.6 作用域插槽与具名插槽结合
- 4.7 Vue 3中的作用域插槽
- 4.8 作用域插槽的应用场景
- 5. 插槽高级用法
- 5.1 渲染函数中的插槽
- 5.2 插槽包装器模式
- 5.3 函数式组件中的插槽
- 5.4 递归插槽
- 5.5 透传插槽
- 5.6 插槽与v-for结合
- 6. 插槽最佳实践与常见问题
- 6.1 插槽最佳实践
- 6.2 常见问题与解决方案
- 6.3 Vue 3中的插槽新特性
- 7. 总结
1. 插槽简介
1.1 什么是插槽?
插槽(Slots)是Vue提供的一种内容分发机制,允许我们向组件内部传递内容。通俗地说,插槽就像是组件中的"占位符",你可以在使用组件时,在这个"占位符"中填充任何你想要显示的内容。
插槽使组件变得更加灵活和可复用,因为它允许使用者决定组件内部的部分内容。
1.2 为什么需要插槽?
想象一下,如果没有插槽,当我们需要创建一个按钮组件,可能会出现这样的情况:
// 没有插槽的按钮组件
Vue.component('my-button', {
template: '<button class="btn">点击我</button>'
})
这个组件只能显示"点击我"这个文本。如果我们想要显示其他文本,就需要通过属性传递:
Vue.component('my-button', {
props: ['text'],
template: '<button class="btn">{{ text }}</button>'
})
<my-button text="保存"></my-button>
<my-button text="取消"></my-button>
但是,如果我们想要按钮内部显示复杂的HTML结构(如图标+文字),上面的方法就不够灵活了。这时插槽就派上用场了:
Vue.component('my-button', {
template: '<button class="btn"><slot></slot></button>'
})
<my-button>
<i class="icon-save"></i> 保存
</my-button>
<my-button>
<i class="icon-cancel"></i> 取消
</my-button>
通过插槽,我们可以在不修改组件本身的情况下,灵活地决定组件内容。
1.3 插槽的基本语法
在Vue中,插槽使用<slot></slot>
标签定义。一个简单的带插槽的组件示例如下:
Vue.component('alert-box', {
template: `
<div class="alert-box">
<strong>重要提示!</strong>
<slot></slot>
</div>
`
})
使用该组件时,我们可以在组件标签内添加内容,这些内容将替换组件模板中的<slot></slot>
部分:
<alert-box>
您的账号已被锁定,请联系管理员。
</alert-box>
渲染结果:
<div class="alert-box">
<strong>重要提示!</strong>
您的账号已被锁定,请联系管理员。
</div>
2. 默认插槽
2.1 什么是默认插槽?
默认插槽是最基本的插槽类型,就是我们上面看到的例子。它不需要名字,是组件中的默认内容分发位置。
2.2 默认插槽语法
使用<slot></slot>
标签定义一个默认插槽:
Vue.component('my-component', {
template: `
<div>
<h2>组件标题</h2>
<slot></slot>
</div>
`
})
使用该组件:
<my-component>
<p>这是一些默认插槽内容</p>
<p>可以有多个元素</p>
</my-component>
渲染结果:
<div>
<h2>组件标题</h2>
<p>这是一些默认插槽内容</p>
<p>可以有多个元素</p>
</div>
2.3 插槽默认内容
有时候,我们希望在使用者没有提供内容时,插槽能显示一些默认内容。这时,我们可以在<slot>
标签内部添加内容作为默认值:
Vue.component('submit-button', {
template: `
<button type="submit">
<slot>提交</slot>
</button>
`
})
使用该组件:
<!-- 使用默认内容 -->
<submit-button></submit-button>
<!-- 替换默认内容 -->
<submit-button>保存</submit-button>
渲染结果:
<!-- 使用默认内容的渲染结果 -->
<button type="submit">提交</button>
<!-- 替换默认内容的渲染结果 -->
<button type="submit">保存</button>
2.4 默认插槽实例:创建一个卡片组件
让我们创建一个更实用的例子 - 一个卡片组件,它有标题和内容区域,其中内容区域使用默认插槽:
Vue.component('card', {
props: ['title'],
template: `
<div class="card">
<div class="card-header">
<h3>{{ title }}</h3>
</div>
<div class="card-body">
<slot>没有内容提供</slot>
</div>
</div>
`
})
使用该组件:
<card title="欢迎">
<p>感谢您访问我们的网站!</p>
<button>开始探索</button>
</card>
<card title="提示"></card>
渲染结果:
<div class="card">
<div class="card-header">
<h3>欢迎</h3>
</div>
<div class="card-body">
<p>感谢您访问我们的网站!</p>
<button>开始探索</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>提示</h3>
</div>
<div class="card-body">
没有内容提供
</div>
</div>
2.5 Vue 3中的默认插槽
在Vue 3中,默认插槽的使用方式与Vue 2基本相同,但是组件的定义方式可能不同:
// Vue 3使用setup语法
const Card = {
props: ['title'],
template: `
<div class="card">
<div class="card-header">
<h3>{{ title }}</h3>
</div>
<div class="card-body">
<slot>没有内容提供</slot>
</div>
</div>
`
}
使用组合式API (Composition API):
import { defineComponent } from 'vue'
export default defineComponent({
props: ['title'],
setup(props) {
// 此处是组合式API的逻辑
return () => (
<div class="card">
<div class="card-header">
<h3>{props.title}</h3>
</div>
<div class="card-body">
{/* JSX中的插槽表示 */}
{this.$slots.default?.() || '没有内容提供'}
</div>
</div>
)
}
})
2.6 默认插槽的应用场景
默认插槽非常适合以下场景:
- 包装组件 - 当你需要在某些内容周围添加一致的样式或结构
- 布局组件 - 例如容器、卡片或面板
- 功能性组件 - 如模态框、警告框,其中内容可变但行为一致
例如,一个简单的模态框组件:
Vue.component('modal-dialog', {
props: ['isOpen'],
template: `
<div v-if="isOpen" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<button @click="$emit('close')" class="close-btn">×</button>
</div>
<div class="modal-body">
<slot>这是模态框的默认内容</slot>
</div>
</div>
</div>
`
})
使用该组件:
<modal-dialog :is-open="showModal" @close="showModal = false">
<h2>重要通知</h2>
<p>您的订单已经成功提交!</p>
<button @click="showModal = false">确定</button>
</modal-dialog>
3. 具名插槽
3.1 什么是具名插槽?
当我们需要在组件中定义多个插槽时,就需要使用具名插槽。具名插槽允许我们将不同的内容分发到组件模板的不同位置。
例如,一个典型的页面布局组件可能需要头部、侧边栏、主内容区和底部等多个插槽。
3.2 具名插槽语法
在Vue 2.6之前,具名插槽使用slot
属性:
Vue.component('layout', {
template: `
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
})
<layout>
<h1 slot="header">网站标题</h1>
<p>主内容区域</p>
<p slot="footer">版权信息 © 2023</p>
</layout>
在Vue 2.6及以后的版本中,引入了v-slot
指令,替代了slot
属性:
<layout>
<template v-slot:header>
<h1>网站标题</h1>
</template>
<p>主内容区域</p>
<template v-slot:footer>
<p>版权信息 © 2023</p>
</template>
</layout>
注意:在Vue 2.6以后,
v-slot
指令只能用在<template>
标签上(除了一种特殊情况,后面会讲到)。
3.3 具名插槽缩写
v-slot
指令可以缩写为#
,这使得模板更加简洁:
<layout>
<template #header>
<h1>网站标题</h1>
</template>
<p>主内容区域</p>
<template #footer>
<p>版权信息 © 2023</p>
</template>
</layout>
3.4 默认插槽的显式名称
默认插槽其实有一个隐含的名称default
。所以以下两种写法是等价的:
<!-- 隐式默认插槽 -->
<layout>
<p>主内容区域</p>
</layout>
<!-- 显式默认插槽 -->
<layout>
<template #default>
<p>主内容区域</p>
</template>
</layout>
3.5 具名插槽实例:页面布局组件
让我们创建一个更完整的页面布局组件:
Vue.component('page-layout', {
template: `
<div class="page">
<header class="page-header">
<slot name="header">
<h1>默认标题</h1>
</slot>
</header>
<nav class="page-sidebar">
<slot name="sidebar">
<ul>
<li>默认导航项1</li>
<li>默认导航项2</li>
</ul>
</slot>
</nav>
<main class="page-content">
<slot></slot>
</main>
<footer class="page-footer">
<slot name="footer">
<p>默认页脚内容</p>
</slot>
</footer>
</div>
`
})
使用该组件:
<page-layout>
<template #header>
<div class="custom-header">
<h1>我的应用</h1>
<p>欢迎访问!</p>
</div>
</template>
<template #sidebar>
<nav>
<a href="/home">首页</a>
<a href="/about">关于我们</a>
<a href="/contact">联系我们</a>
</nav>
</template>
<div>
<h2>主要内容</h2>
<p>这是页面的主要内容区域...</p>
</div>
<template #footer>
<div class="custom-footer">
<p>© 2023 我的应用. 保留所有权利.</p>
<div class="social-links">
<a href="#">Facebook</a>
<a href="#">Twitter</a>
</div>
</div>
</template>
</page-layout>
这个例子展示了如何创建一个灵活的页面布局组件,使用者可以自定义页面的各个部分。
3.6 Vue 3中的具名插槽
Vue 3中具名插槽的用法与Vue 2.6+基本一致,使用v-slot
指令或其缩写#
:
<page-layout>
<template #header>
<h1>Vue 3 应用</h1>
</template>
<template #default>
<p>主内容区域</p>
</template>
<template #footer>
<p>Vue 3 页脚</p>
</template>
</page-layout>
在使用渲染函数或JSX时,Vue 3提供了更简洁的方式:
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
return () => (
<div class="page">
<header class="page-header">
{this.$slots.header?.() || <h1>默认标题</h1>}
</header>
<main class="page-content">
{this.$slots.default?.()}
</main>
<footer class="page-footer">
{this.$slots.footer?.() || <p>默认页脚内容</p>}
</footer>
</div>
)
}
})
3.7 具名插槽的应用场景
具名插槽特别适合以下场景:
- 复杂布局组件 - 网页布局、仪表盘等需要多个内容区域的组件
- 复合组件 - 如带有头部、内容和底部的卡片
- 对话框组件 - 需要自定义标题、内容和按钮区域
- 标签面板组件 - 自定义标签头和内容
例如,一个标签组件:
Vue.component('tabbed-panel', {
template: `
<div class="tabs">
<div class="tab-headers">
<slot name="tabs"></slot>
</div>
<div class="tab-content">
<slot name="content"></slot>
</div>
</div>
`
})
使用该组件:
<tabbed-panel>
<template #tabs>
<button class="tab" @click="activeTab = 'tab1'">用户信息</button>
<button class="tab" @click="activeTab = 'tab2'">订单历史</button>
</template>
<template #content>
<div v-if="activeTab === 'tab1'">
<h3>用户信息</h3>
<p>用户详细信息显示在这里...</p>
</div>
<div v-else-if="activeTab === 'tab2'">
<h3>订单历史</h3>
<p>用户订单历史显示在这里...</p>
</div>
</template>
</tabbed-panel>
3.8 动态插槽名
从Vue 2.6.0开始,我们可以使用动态插槽名,这在需要根据数据动态确定插槽位置时非常有用:
data() {
return {
dynamicSlot: 'header'
}
}
<base-layout>
<template v-slot:[dynamicSlot]>
动态插槽名称示例
</template>
</base-layout>
使用缩写形式:
<base-layout>
<template #[dynamicSlot]>
动态插槽名称示例
</template>
</base-layout>
动态插槽名例子:标签切换
<div>
<div class="tab-buttons">
<button
v-for="tab in tabs"
:key="tab.id"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<tabbed-content>
<template #[currentTab]>
当前显示的是:{{ currentTab }} 对应的内容
</template>
</tabbed-content>
</div>
data() {
return {
currentTab: 'tab1',
tabs: [
{ id: 'tab1', name: '选项1' },
{ id: 'tab2', name: '选项2' },
{ id: 'tab3', name: '选项3' }
]
}
}
4. 作用域插槽
4.1 什么是作用域插槽?
作用域插槽(Scoped Slots)是Vue中最强大的插槽类型,它允许组件将自己内部的数据传递给插槽内容使用。这意味着插槽内容可以访问子组件中的数据,而不仅仅是父组件的数据。
通俗地说,普通插槽是把内容从父组件传给子组件,而作用域插槽则是让子组件决定要传什么数据给插槽内容使用。
4.2 为什么需要作用域插槽?
想象这样一个场景:你有一个展示用户列表的组件,但是你希望能够自定义每个用户的具体展示方式。
普通的做法可能是:
Vue.component('user-list', {
props: ['users'],
template: `
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
</li>
</ul>
`
})
这个组件虽然展示了用户列表,但展示格式是固定的。如果我们想让使用者决定如何展示每个用户,就需要用到作用域插槽。
4.3 作用域插槽语法
在Vue 2.6之前,作用域插槽使用slot-scope
属性:
Vue.component('user-list', {
props: ['users'],
template: `
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user">
<!-- 默认内容 -->
{{ user.name }}
</slot>
</li>
</ul>
`
})
<user-list :users="users">
<template slot-scope="slotProps">
{{ slotProps.user.name }} ({{ slotProps.user.email }})
</template>
</user-list>
在Vue 2.6及以后版本中,使用v-slot
指令替代了slot-scope
属性:
<user-list :users="users">
<template v-slot:default="slotProps">
{{ slotProps.user.name }} ({{ slotProps.user.email }})
</template>
</user-list>
缩写形式:
<user-list :users="users">
<template #default="slotProps">
{{ slotProps.user.name }} ({{ slotProps.user.email }})
</template>
</user-list>
如果组件只有默认插槽,还可以直接在组件上使用v-slot
:
<user-list :users="users" v-slot="slotProps">
{{ slotProps.user.name }} ({{ slotProps.user.email }})
</user-list>
4.4 解构插槽Props
作用域插槽的props可以使用解构赋值语法,使代码更简洁:
<user-list :users="users">
<template #default="{ user }">
{{ user.name }} ({{ user.email }})
</template>
</user-list>
还可以指定默认值:
<user-list :users="users">
<template #default="{ user = { name: '未知用户', email: '' } }">
{{ user.name }} ({{ user.email }})
</template>
</user-list>
4.5 作用域插槽实例:自定义列表渲染
让我们创建一个更完整的例子,一个可定制的列表组件:
Vue.component('fancy-list', {
props: ['items', 'itemKey'],
template: `
<div class="fancy-list">
<div v-if="items.length === 0" class="empty-message">
<slot name="empty">
没有数据可显示
</slot>
</div>
<ul v-else>
<li v-for="(item, index) in items" :key="itemKey ? item[itemKey] : index" class="list-item">
<slot name="item" :item="item" :index="index">
<!-- 默认仅显示项目文本 -->
{{ item.text || item }}
</slot>
</li>
</ul>
</div>
`
})
使用该组件:
<fancy-list :items="users" item-key="id">
<!-- 自定义空状态 -->
<template #empty>
<div class="empty-state">
<i class="icon icon-user"></i>
<p>暂无用户数据</p>
<button @click="loadUsers">加载用户</button>
</div>
</template>
<!-- 自定义每个项的渲染方式 -->
<template #item="{ item, index }">
<div class="user-card">
<img :src="item.avatar" :alt="item.name" class="avatar">
<div class="user-details">
<h3>{{ item.name }}</h3>
<p>{{ item.email }}</p>
<p class="user-index">用户 #{{ index + 1 }}</p>
</div>
<button @click="editUser(item)">编辑</button>
</div>
</template>
</fancy-list>
这个例子展示了如何使用作用域插槽创建一个高度可定制的列表组件。使用者可以自定义列表为空时的显示内容,以及每个列表项的显示方式。
4.6 作用域插槽与具名插槽结合
作用域插槽和具名插槽可以结合使用,创建更强大的组件:
Vue.component('data-table', {
props: ['items', 'columns'],
template: `
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.id">
<slot name="header" :column="column">
{{ column.label }}
</slot>
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td v-for="column in columns" :key="column.id">
<slot :name="column.id" :item="item" :column="column">
{{ item[column.id] }}
</slot>
</td>
</tr>
</tbody>
</table>
`
})
使用该组件:
<data-table :items="users" :columns="columns">
<!-- 自定义表头 -->
<template #header="{ column }">
<span class="column-title">{{ column.label }}</span>
<i v-if="column.sortable" class="sort-icon"></i>
</template>
<!-- 自定义名字列 -->
<template #name="{ item }">
<a :href="`/users/${item.id}`">{{ item.name }}</a>
</template>
<!-- 自定义状态列 -->
<template #status="{ item }">
<span :class="['status', item.status]">{{ formatStatus(item.status) }}</span>
</template>
<!-- 自定义操作列 -->
<template #actions="{ item }">
<button @click="editUser(item)">编辑</button>
<button @click="deleteUser(item)">删除</button>
</template>
</data-table>
data() {
return {
users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com', status: 'active' },
{ id: 2, name: '李四', email: 'lisi@example.com', status: 'inactive' }
],
columns: [
{ id: 'name', label: '姓名', sortable: true },
{ id: 'email', label: '邮箱' },
{ id: 'status', label: '状态' },
{ id: 'actions', label: '操作' }
]
}
},
methods: {
formatStatus(status) {
return status === 'active' ? '活跃' : '非活跃';
},
editUser(user) {
// 编辑用户
},
deleteUser(user) {
// 删除用户
}
}
4.7 Vue 3中的作用域插槽
Vue 3中的作用域插槽语法与Vue 2.6+一致:
<fancy-list :items="users">
<template #item="{ item }">
{{ item.name }}
</template>
</fancy-list>
在Vue 3的组合式API中,可以这样访问插槽:
import { defineComponent } from 'vue'
export default defineComponent({
props: ['items'],
setup(props) {
return () => (
<div>
<ul>
{props.items.map((item, index) => (
<li key={item.id}>
{this.$slots.item
? this.$slots.item({ item, index })
: item.text}
</li>
))}
</ul>
</div>
)
}
})
4.8 作用域插槽的应用场景
作用域插槽特别适合以下场景:
- 列表组件 - 允许自定义每个列表项的渲染方式
- 表格组件 - 允许自定义表格单元格的显示
- 表单元素包装器 - 提供上下文信息(如验证状态)
- 无限滚动组件 - 允许自定义每个加载项
- 引导式教程组件 - 能够访问教程状态和控件
例如,一个表单字段包装器组件:
Vue.component('form-field', {
props: ['value', 'label', 'errors'],
template: `
<div class="form-field" :class="{ 'has-error': hasError }">
<label>{{ label }}</label>
<slot :value="value" :update="update" :hasError="hasError" :errors="errors">
</slot>
<div v-if="hasError" class="error-message">{{ errors[0] }}</div>
</div>
`,
computed: {
hasError() {
return this.errors && this.errors.length > 0;
}
},
methods: {
update(newValue) {
this.$emit('input', newValue);
}
}
})
使用该组件:
<form-field v-model="username" label="用户名" :errors="errors.username">
<template #default="{ value, update, hasError }">
<input
type="text"
:value="value"
@input="update($event.target.value)"
:class="{ 'input-error': hasError }"
>
</template>
</form-field>
<form-field v-model="gender" label="性别" :errors="errors.gender">
<template #default="{ value, update }">
<select :value="value" @change="update($event.target.value)">
<option value="">请选择</option>
<option value="male">男</option>
<option value="female">女</option>
</select>
</template>
</form-field>
这个例子展示了如何使用作用域插槽创建灵活的表单字段包装器,它提供了标签、错误处理逻辑,同时允许使用者自定义实际的输入元素。
5. 插槽高级用法
5.1 渲染函数中的插槽
除了在模板中使用插槽,我们还可以在渲染函数(Render Functions)中使用插槽。这对于需要以编程方式处理插槽内容的场景非常有用。
在Vue 2中访问插槽:
Vue.component('example', {
render(h) {
// 访问默认插槽
const defaultSlot = this.$slots.default;
// 访问具名插槽
const headerSlot = this.$slots.header;
// 访问作用域插槽
const contentSlot = this.$scopedSlots.content;
return h('div', [
// 具名插槽
h('header', [
headerSlot || h('h2', '默认标题')
]),
// 内容区域(作用域插槽)
h('main', [
contentSlot
? contentSlot({ message: '来自子组件的数据' })
: h('p', '默认内容')
]),
// 默认插槽
h('footer', [
defaultSlot || h('p', '默认页脚')
])
]);
}
})
在Vue 3中,插槽访问方式有所变化:
import { h } from 'vue'
export default {
render() {
// 在Vue 3中,所有插槽都在this.$slots中,并且都是函数
const defaultSlotContent = this.$slots.default?.() || h('div', '默认内容');
const headerSlotContent = this.$slots.header?.() || h('h2', '默认标题');
// 作用域插槽也是函数,可以传入参数
const contentSlotContent = this.$slots.content
? this.$slots.content({ message: '来自子组件的数据' })
: h('p', '默认内容');
return h('div', [
h('header', [headerSlotContent]),
h('main', [contentSlotContent]),
h('footer', [defaultSlotContent])
]);
}
}
5.2 插槽包装器模式
插槽包装器模式(Slot Wrapper Pattern)是一种设计模式,它允许组件通过插槽对其子组件进行增强或修改。
例如,创建一个为内容添加淡入动画的包装器组件:
Vue.component('fade-in', {
props: {
duration: {
type: Number,
default: 1000
}
},
data() {
return {
visible: false
};
},
mounted() {
this.$nextTick(() => {
this.visible = true;
});
},
template: `
<transition :duration="duration" name="fade">
<div v-if="visible">
<slot></slot>
</div>
</transition>
`
})
使用该组件:
<fade-in :duration="2000">
<div class="content">
<h1>欢迎!</h1>
<p>这个内容会淡入显示</p>
</div>
</fade-in>
另一个例子是创建一个"悬停卡片"组件,当鼠标悬停在内容上时显示额外信息:
Vue.component('hover-card', {
data() {
return {
isHovering: false
};
},
template: `
<div
class="hover-container"
@mouseenter="isHovering = true"
@mouseleave="isHovering = false"
>
<div class="hover-trigger">
<slot name="trigger"></slot>
</div>
<div class="hover-content" v-if="isHovering">
<slot name="content">
<p>悬停内容</p>
</slot>
</div>
</div>
`
})
使用该组件:
<hover-card>
<template #trigger>
<button>鼠标悬停在我上面</button>
</template>
<template #content>
<div class="tooltip">
<h3>详细信息</h3>
<p>这是悬停时显示的额外信息</p>
</div>
</template>
</hover-card>
5.3 函数式组件中的插槽
函数式组件是一种特殊的组件,它没有状态和实例,因此性能更高。在Vue 2中,我们这样在函数式组件中使用插槽:
Vue.component('functional-component', {
functional: true,
render(h, context) {
// 在Vue 2中,插槽通过context.slots()或context.scopedSlots访问
const slots = context.slots();
const scopedSlots = context.scopedSlots;
return h('div', {
class: 'wrapper'
}, [
slots.header || h('header', 'Default Header'),
scopedSlots.content
? scopedSlots.content({ message: 'Hello' })
: h('p', 'Default Content'),
slots.footer || h('footer', 'Default Footer')
]);
}
})
在Vue 3中,函数式组件是普通的函数,插槽的访问方式也有所不同:
import { h } from 'vue'
const FunctionalComponent = (props, { slots }) => {
return h('div', { class: 'wrapper' }, [
slots.header?.() || h('header', 'Default Header'),
slots.content
? slots.content({ message: 'Hello' })
: h('p', 'Default Content'),
slots.footer?.() || h('footer', 'Default Footer')
]);
}
export default FunctionalComponent;
5.4 递归插槽
递归插槽是一种高级技术,它允许创建可以递归嵌套的组件,适用于表示树状结构(如目录树、评论嵌套等)。
例如,一个树形组件:
Vue.component('tree-item', {
props: {
item: Object
},
template: `
<li>
<div class="tree-node">
<slot name="node" :item="item">
{{ item.name }}
</slot>
</div>
<ul v-if="item.children && item.children.length">
<tree-item
v-for="(child, index) in item.children"
:key="index"
:item="child"
>
<template v-for="slotName in Object.keys($slots)" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
</tree-item>
</ul>
</li>
`
})
使用该组件:
<ul class="tree">
<tree-item :item="treeData">
<template #node="{ item }">
<span class="node-content">
<i :class="getIconClass(item)"></i>
{{ item.name }}
<span class="node-count" v-if="item.children">
({{ item.children.length }})
</span>
</span>
<button v-if="item.url" @click="openUrl(item.url)">打开</button>
</template>
</tree-item>
</ul>
data() {
return {
treeData: {
name: '根目录',
children: [
{ name: '文档', children: [
{ name: 'Vue教程.pdf', url: '/docs/vue.pdf' },
{ name: 'React教程.pdf', url: '/docs/react.pdf' }
]},
{ name: '图片', children: [
{ name: '头像.png', url: '/images/avatar.png' },
{ name: '背景.jpg', url: '/images/background.jpg' }
]},
{ name: '视频', children: [
{ name: '介绍.mp4', url: '/videos/intro.mp4' }
]}
]
}
};
},
methods: {
getIconClass(item) {
return item.children ? 'icon-folder' : 'icon-file';
},
openUrl(url) {
window.open(url, '_blank');
}
}
5.5 透传插槽
有时候我们需要将从父组件接收到的插槽内容传递给子组件,这称为"透传插槽"(Slot Passthrough):
Vue.component('layout-wrapper', {
template: `
<div class="layout">
<site-header>
<!-- 将header插槽透传给site-header组件 -->
<template #logo>
<slot name="header-logo"></slot>
</template>
<template #nav>
<slot name="header-nav"></slot>
</template>
</site-header>
<main>
<slot></slot>
</main>
<site-footer>
<!-- 将footer插槽透传给site-footer组件 -->
<slot name="footer"></slot>
</site-footer>
</div>
`
})
使用该组件:
<layout-wrapper>
<template #header-logo>
<img src="/logo.png" alt="Logo">
</template>
<template #header-nav>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
</nav>
</template>
<div class="content">
<h1>页面内容</h1>
<p>这是主要内容...</p>
</div>
<template #footer>
<p>版权所有 © 2023</p>
</template>
</layout-wrapper>
5.6 插槽与v-for结合
可以在使用v-for的元素上使用插槽,动态创建多个插槽内容:
Vue.component('tabs', {
props: ['tabs'],
data() {
return {
activeTab: null
};
},
created() {
if (this.tabs.length > 0) {
this.activeTab = this.tabs[0].id;
}
},
template: `
<div class="tabs-container">
<div class="tabs-header">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="{ active: activeTab === tab.id }"
>
{{ tab.name }}
</button>
</div>
<div class="tab-content">
<template v-for="tab in tabs">
<div
:key="tab.id"
v-if="activeTab === tab.id"
class="tab-pane"
>
<slot :name="tab.id" :tab="tab">
<!-- 默认内容 -->
<p>{{ tab.name }} 的默认内容</p>
</slot>
</div>
</template>
</div>
</div>
`
})
使用该组件:
<tabs :tabs="tabItems">
<template v-for="tab in tabItems" #[tab.id]="{ tab }">
<div :key="tab.id" class="custom-tab-content">
<h3>{{ tab.name }}</h3>
<div v-html="tab.content"></div>
<button v-if="tab.hasButton" @click="handleTabAction(tab)">
{{ tab.buttonText }}
</button>
</div>
</template>
</tabs>
data() {
return {
tabItems: [
{ id: 'tab1', name: '介绍', content: '<p>这是介绍内容...</p>', hasButton: false },
{ id: 'tab2', name: '功能', content: '<p>这是功能列表...</p>', hasButton: true, buttonText: '了解更多' },
{ id: 'tab3', name: '定价', content: '<p>这是价格信息...</p>', hasButton: true, buttonText: '立即购买' }
]
};
},
methods: {
handleTabAction(tab) {
console.log(`Tab ${tab.id} button clicked`);
// 根据不同的tab执行不同的操作
}
}
6. 插槽最佳实践与常见问题
6.1 插槽最佳实践
-
提供默认内容
始终为插槽提供合理的默认内容,这样即使用户没有提供插槽内容,组件也能正常工作:
<slot name="header"> <h2>默认标题</h2> </slot>
-
使用具名插槽而非多个默认插槽
如果组件需要多个插槽,使用具名插槽而不是多个默认插槽嵌套:
<!-- 好的做法 --> <div> <slot name="header"></slot> <slot name="content"></slot> <slot name="footer"></slot> </div> <!-- 避免这样做 --> <div> <div> <slot></slot> </div> <div> <slot></slot> </div> </div>
-
明确的插槽名称
使用描述性强的插槽名称,避免模糊不清的名称:
<!-- 好的命名 --> <slot name="item-header"></slot> <slot name="item-content"></slot> <!-- 避免模糊的命名 --> <slot name="top"></slot> <slot name="middle"></slot>
-
为作用域插槽提供有意义的prop名
当使用作用域插槽时,提供有意义的prop名称:
<!-- 好的做法 --> <slot name="user" :user="userObj" :can-edit="hasEditPermission"></slot> <!-- 避免这样做 --> <slot name="user" :a="userObj" :b="hasEditPermission"></slot>
-
使用解构简化作用域插槽
在使用作用域插槽时,利用解构简化代码:
<!-- 简化前 --> <template #item="slotProps"> {{ slotProps.item.name }} </template> <!-- 简化后 --> <template #item="{ item }"> {{ item.name }} </template>
-
合理使用插槽默认值
给作用域插槽的解构属性提供默认值:
<template #user="{ user = { name: '游客' } }"> {{ user.name }} </template>
-
使用插槽作为API的一部分
将插槽视为组件API的一部分,在文档中明确记录每个插槽的用途和可用的作用域属性。
6.2 常见问题与解决方案
-
动态组件中的插槽
使用
<component>
动态组件时,插槽内容会被传递给当前激活的组件:<component :is="currentComponent"> <template #header> <h1>动态组件标题</h1> </template> <p>内容会被传递给当前激活的组件</p> </component>
-
使用v-if/v-else与插槽
在使用
v-if
/v-else
与插槽时,确保在切换时正确处理插槽:<div v-if="condition"> <slot name="one"></slot> </div> <div v-else> <slot name="two"></slot> </div>
使用该组件:
<my-component :condition="value"> <template #one>内容一</template> <template #two>内容二</template> </my-component>
-
重复使用相同插槽内容
如果需要在多个位置使用相同的插槽内容,可以使用变量保存插槽内容:
const CustomBtn = { template: ` <div> <header> <slot name="action"></slot> </header> <footer> <slot name="action"></slot> </footer> </div> ` }
不过,更好的方式是使用两个不同的插槽名称,然后用户决定是否提供相同的内容:
const BetterBtn = { template: ` <div> <header> <slot name="header-action"></slot> </header> <footer> <slot name="footer-action"></slot> </footer> </div> ` }
-
插槽内容更新问题
插槽内容由父组件提供,但在子组件中使用。这意味着插槽内容具有父组件的上下文,而不是子组件的上下文:
Vue.component('parent-scope', { data() { return { message: '来自父组件的消息' }; }, template: ` <div> <child-component> <!-- 这里使用的是父组件的message --> {{ message }} </child-component> </div> ` }); Vue.component('child-component', { data() { return { message: '来自子组件的消息' }; }, template: ` <div> <slot></slot> </div> ` });
渲染结果会显示"来自父组件的消息",而不是"来自子组件的消息"。
-
作用域插槽性能考虑
作用域插槽比普通插槽稍微慢一些,因为它们需要在渲染时创建函数。不过在大多数应用中,这种差异是可以忽略的。只有在极端性能要求下,才需要考虑这一点。
-
处理未提供的插槽
始终检查插槽是否存在,特别是在高阶组件中:
// 在render函数中 render() { return h('div', [ this.$slots.header ? this.$slots.header() : h('h2', '默认标题') ]); }
6.3 Vue 3中的插槽新特性
Vue 3引入了一些插槽的新特性和变化:
-
统一的插槽语法
Vue 3中,所有插槽(包括默认插槽、具名插槽和作用域插槽)都统一使用
v-slot
指令或其缩写#
。 -
所有插槽都是函数
在Vue 3中,
this.$slots
中的所有插槽都是函数,需要调用才能获取VNode:// Vue 2 render() { return h('div', this.$slots.default); } // Vue 3 render() { return h('div', this.$slots.default()); }
-
片段支持
Vue 3支持片段(Fragments),这意味着组件可以有多个根节点,这也适用于插槽内容:
<my-component> <h1>标题</h1> <p>段落一</p> <p>段落二</p> <!-- 不需要额外的包装元素 --> </my-component>
7. 总结
Vue的插槽系统是组件复用和自定义的强大工具。通过本教程,我们学习了:
- 默认插槽 - 用于单一内容分发
- 具名插槽 - 用于将内容分发到组件的多个位置
- 作用域插槽 - 让子组件向插槽内容提供数据
- 动态插槽名 - 允许根据数据动态确定插槽名
- 高级用法 - 包括渲染函数中的插槽、递归插槽等
插槽使Vue组件变得更加灵活和可复用,是构建高质量组件库的关键工具。熟练掌握插槽,能够帮助你创建出更加灵活、可定制的组件,大大提高代码的复用性和开发效率。
无论是简单的按钮组件,还是复杂的数据表格、布局系统,插槽都能让你的组件适应各种不同的使用场景,成为真正的"乐高积木",可以组合构建出复杂的用户界面。