在实际开发中,我们通常使用 Props 来完成父组件向子组件的数据传递。Props 非常适合传递字符串、数字、对象、数组以及函数等数据。
但有些场景下,父组件希望传递给子组件的不只是数据,而是一段完整的模板内容,例如按钮、表单、图片,甚至其他组件。对于这类需要传递界面结构的需求,单纯使用 Props 往往不够灵活。
此时,Vue 提供了另一种内容分发机制 —— Slot(插槽)。
示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 Slot 示例 (Options API)</title>
<script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
<style>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
width: 320px;
margin: 20px auto;
box-shadow: 04px6pxrgba(0,0,0,0.1);
font-family: sans-serif;
}
.card-header {
background-color: #f5f5f5;
padding: 10px15px;
border-bottom: 1px solid #e0e0e0;
font-weight: bold;
}
.card-body {
padding: 15px;
color: #333;
line-height: 1.5;
}
.card-footer {
background-color: #fafafa;
padding: 10px15px;
border-top: 1px solid #e0e0e0;
text-align: right;
}
</style>
</head>
<body>
<div id="app">
<h2 style="text-align: center;">Vue 3 插槽演示 (Options API)</h2>
<my-card>
<template v-slot:header>
<span>自定义卡片标题</span>
</template>
<p>这是通过<b>默认插槽</b>传递进来的正文内容。没有使用 setup 语法,完全采用传统的 data 和 methods 结构。</p>
<template #footer>
<button @click="handleClick">点击触发父组件事件</button>
</template>
</my-card>
</div>
<script>
// 从全局对象中获取 createApp
const { createApp } = Vue;
// 1. 定义子组件 (使用 Options API)
constMyCard = {
name: 'MyCard',
template: `
<div class="card">
<div class="card-header">
<slot name="header">默认标题</slot>
</div>
<div class="card-body">
<slot></slot>
</div>
<div class="card-footer">
<slot name="footer">默认页脚</slot>
</div>
</div>
`
};
// 2. 创建根应用 (使用 Options API)
constApp = {
// 注册局部组件
components: {
'my-card': MyCard
},
// 数据源
data() {
return {
message: '来自父组件的数据'
}
},
// 方法
methods: {
handleClick() {
alert('点击了插槽中的按钮!提示信息:' + this.message);
}
}
};
// 3. 挂载应用
createApp(App).mount('#app');
</script>
</body>
</html>如上面的例子所示,子组件 MyCard 一共定义了三个插槽:
<slot name="header">默认标题</slot>
<slot></slot>
<slot name="footer">默认页脚</slot>分别对应:
具名插槽
当 <slot> 通过 name 属性指定名称时,这种插槽称为具名插槽。
例如:
<slot name="header">默认标题</slot>
父组件向该插槽传递内容时,需要使用:
<template v-slot:header>
自定义标题
</template>其中 v-slot:header 表示将模板内容插入到名称为 header 的插槽中。
由于 v-slot 使用频率较高,Vue 提供了简写语法:
<template #header>
自定义标题
</template>因此v-slot:header 等价于 #header
默认插槽
没有指定 name 的插槽称为默认插槽(也叫匿名插槽)。
<slot></slot>
或者
<slot name="default"></slot>
两者表示的都是默认插槽。
父组件使用默认插槽时有两种写法。
方式一:显式指定 default
<my-card>
<!--v-slot:default 也可以-->
<template #default>
<p>正文内容</p>
</template>
</my-card>方式二:直接写内容(最常用)
如果只有默认插槽,可以直接省略 template:
<my-card>
<p>正文内容</p>
</my-card>Vue 会自动将这部分内容放入默认插槽中。
作用域插槽
普通插槽:父组件只能传模板,拿不到子组件内部数据
作用域插槽:子组件传数据,父组件使用这些数据自定义渲染内容
作用域插槽允许父组件在传递插槽内容时,直接访问子组件暴露的数据。子组件通过插槽 Props 将内部数据传递给父组件,而父组件则使用这些数据来自定义插槽内容的渲染方式。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 作用域插槽 (无 Setup 选项式 API)</title>
<script src="https://unpkg.com/vue@3.5.17/dist/vue.global.js"></script>
<style>
body { font-family: sans-serif; padding: 20px; background: #f5f5f5; }
.card { background: white; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.user-item { padding: 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.admin-tag { background: #52c41a; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
.guest-tag { background: #bfbfbf; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
</style>
</head>
<body>
<div id="app" class="card">
<h2>👥 父组件(用户管理面板)</h2>
<p>当前系统状态:正常</p>
<hr>
<user-list :users="userList">
<template #default="slotProps">
<div>
<strong>{{ slotProps.data.name }}</strong>
<span v-if="slotProps.data.role === 'admin'" class="admin-tag">管理员</span>
<span v-else class="guest-tag">访客</span>
</div>
<div>联系方式:{{ slotProps.data.email }}</div>
</template>
</user-list>
</div>
<script>
// 1. 定义子组件 (纯 Options API 风格)
constUserList = {
props: ['users'],
// 在 template 中,通过 :data="user" 把当前循环的用户对象传给外层
template: `
<div style="margin-transform: translateY( 20px;">
<h3>📦 我是子组件(负责循环和布局)</h3>
<ul>
<li v-for="user in users" :key="user.id" class="user-item)">
<slot :data="user">
{{ user.name }}
</slot>
</li>
</ul>
</div>
`
};
// 2. 创建根实例(父组件,同样使用传统的 data 选项)
Vue.createApp({
components: {
'user-list': UserList
},
// 使用传统的 data 选项返回响应式数据
data() {
return {
userList: [
{ id: 1, name: '张三', role: 'admin', email: 'zs@example.com' },
{ id: 2, name: '李四', role: 'guest', email: 'ls@example.com' },
{ id: 3, name: '王五', role: 'admin', email: 'ww@example.com' }
]
}
}
}).mount('#app');
</script>
</body>
</html>在这个示例中,父组件通过 Props 将 userList 传递给子组件,子组件负责遍历用户数据和整体布局。
子组件在作用域插槽中通过 :data="user" 将当前用户对象作为插槽参数暴露出去,父组件则通过 slotProps 获取这些数据,并决定最终的 UI 渲染方式。
作用域插槽的意义在于:它为子组件向父组件提供数据提供了一种机制,使父组件既能访问子组件内部产生的数据,又能保持对渲染结构的完全控制。