Vue 3 动态插槽名与条件插槽的灵活应用完全指南
一、动态插槽名:运行时决定内容去向
1.1 什么是动态插槽名?
动态插槽名使用动态指令参数的语法:v-slot:[dynamicSlotName] 或其简写形式 #[dynamicSlotName]。
1.2 动态插槽名的基础使用
<!-- DynamicTabs.vue -->
<template>
<div class="tab-container">
<!-- 标签页导航 -->
<nav class="tab-nav">
<button
v-for="tab in tabs"
:key="tab.name"
:class="['tab-btn', { active: activeTab === tab.name }]"
@click="activeTab = tab.name"
>
</button>
</nav>
<!-- 动态渲染选中的插槽内容 -->
<div class="tab-content">
<slot :name="activeTab"></slot>
</div>
</div>
</template>
<script setup>
import { ref, defineProps } from "vue";
const props = defineProps({
tabs: {
type: Array,
required: true,
// 期望格式:[{ name: 'tab1', label: '标签1' }, ...]
},
});
const activeTab = ref(props.tabs[0]?.name || "");
</script>
<style scoped>
.tab-container {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.tab-nav {
display: flex;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: none;
cursor: pointer;
color: #666;
transition:
background-color 0.2s,
color 0.2s;
}
.tab-btn:hover {
background-color: #e8e8e8;
color: #333;
}
.tab-btn.active {
background-color: #fff;
color: #42b983;
border-bottom: 2px solid #42b983;
}
.tab-content {
padding: 24px;
background-color: #fff;
min-height: 200px;
}
</style>使用方式:
<!-- ParentComponent.vue -->
<template>
<DynamicTabs :tabs="tabList">
<!-- 动态指定内容渲染到对应插槽 -->
<template #profile>
<h3>个人资料</h3>
<p>这是用户个人资料页面...</p>
</template>
<template #settings>
<h3>账户设置</h3>
<p>这是账户设置页面...</p>
</template>
<template #notifications>
<h3>通知中心</h3>
<p>这是通知中心页面...</p>
</template>
</DynamicTabs>
</template>
<script setup>
import { ref } from "vue";
import DynamicTabs from "./DynamicTabs.vue";
const tabList = ref([
{ name: "profile", label: "个人资料" },
{ name: "settings", label: "账户设置" },
{ name: "notifications", label: "通知中心" },
]);
</script>1.3 使用变量动态指定插槽
<!-- DynamicContent.vue -->
<template>
<div>
<!-- 使用计算属性动态决定插槽名 -->
<slot :name="currentSlotName"></slot>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
const contentType = ref("text");
// 根据contentType动态计算插槽名
const currentSlotName = computed(() => {
return `content-${contentType.value}`;
});
// 暴露方法供父组件调用
defineExpose({
changeContentType: (type) => {
contentType.value = type;
},
});
</script>使用方式:
<template>
<DynamicContent ref="dynamicContentRef">
<template #content-text>
<p>这是文本内容...</p>
</template>
<template #content-image>
<img src="https://via.placeholder.com/400" alt="示例图片" />
</template>
<template #content-video>
<video controls>
<source src="video.mp4" type="video/mp4" />
</video>
</template>
</DynamicContent>
<div class="controls">
<button @click="dynamicContentRef?.changeContentType('text')">文本</button>
<button @click="dynamicContentRef?.changeContentType('image')">图片</button>
<button @click="dynamicContentRef?.changeContentType('video')">视频</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import DynamicContent from "./DynamicContent.vue";
const dynamicContentRef = ref(null);
</script>1.4 父组件中使用动态插槽名
<!-- ParentWithDynamicSlots.vue -->
<template>
<BaseLayout>
<!-- 使用变量动态指定插槽名 -->
<template #[currentSlot]>
<p>动态插槽内容</p>
</template>
<!-- 使用表达式动态指定插槽名 -->
<template #[`section-${sectionIndex}`]>
<p>第 节内容</p>
</template>
</BaseLayout>
</template>
<script setup>
import { ref } from "vue";
import BaseLayout from "./BaseLayout.vue";
const currentSlot = ref("header");
const sectionIndex = ref(1);
</script>二、条件插槽:根据插槽是否存在渲染内容
2.1 为什么需要条件插槽?
有时我们需要根据插槽是否存在来渲染某些内容。例如,当父组件提供了header插槽内容时,我们才渲染header容器;如果没有提供,就不渲染header容器,避免多余的DOM节点和样式。
Vue提供了$slots属性,我们可以在模板中使用它来检查插槽是否存在。
2.2 使用 $slots 属性检查插槽
<!-- ConditionalCard.vue -->
<template>
<div class="card">
<!-- 只有当header插槽存在时才渲染 -->
<header v-if="$slots.header" class="card-header">
<slot name="header" />
</header>
<!-- 只有当默认插槽存在时才渲染 -->
<div v-if="$slots.default" class="card-content">
<slot />
</div>
<!-- 只有当footer插槽存在时才渲染 -->
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</footer>
</div>
</template>
<style scoped>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background-color: #fff;
}
.card-header {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
background-color: #f9f9f9;
}
.card-content {
padding: 24px;
}
.card-footer {
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
background-color: #f9f9f9;
}
</style>2.3 $slots 属性的工作原理
子组件模板渲染
↓
Vue 收集父组件传递的所有插槽
↓
$slots 对象包含所有已定义的插槽
↓
{
header: ƒ(), // header 插槽存在
default: ƒ(), // 默认插槽存在
footer: undefined // footer 插槽不存在
}
↓
使用 v-if="$slots.header" 检查
↓
如果存在,渲染 header 容器
如果不存在,跳过 header 容器
2.4 条件插槽的实际应用
<!-- SmartModal.vue -->
<template>
<Teleport to="body">
<div v-if="visible" class="modal-overlay" @click.self="$emit('close')">
<div class="modal-content">
<!-- 条件渲染:header -->
<header v-if="$slots.header" class="modal-header">
<slot name="header" />
<button class="modal-close" @click="$emit('close')">×</button>
</header>
<!-- 始终渲染:body -->
<main class="modal-body">
<slot />
</main>
<!-- 条件渲染:footer -->
<footer v-if="$slots.footer" class="modal-footer">
<slot name="footer" />
</footer>
</div>
</div>
</Teleport>
</template>
<script setup>
defineProps({
visible: {
type: Boolean,
default: false,
},
});
defineEmits(["close"]);
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: #fff;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #e0e0e0;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.modal-body {
padding: 24px;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #e0e0e0;
}
</style>使用方式:
<template>
<div>
<button @click="showModal = true">打开模态框</button>
<!-- 只提供默认插槽,header 和 footer 不会渲染 -->
<SmartModal :visible="showModal" @close="showModal = false">
<p>这是一个简单的提示框</p>
</SmartModal>
<!-- 提供所有插槽,header 和 footer 会渲染 -->
<SmartModal :visible="showFullModal" @close="showFullModal = false">
<template #header>
<h2>确认删除</h2>
</template>
<p>此操作将永久删除该条目,是否继续?</p>
<template #footer>
<button class="btn" @click="showFullModal = false">取消</button>
<button class="btn btn-danger" @click="handleDelete">确认删除</button>
</template>
</SmartModal>
</div>
</template>
<script setup>
import { ref } from "vue";
import SmartModal from "./SmartModal.vue";
const showModal = ref(false);
const showFullModal = ref(false);
const handleDelete = () => {
console.log("删除操作");
showFullModal.value = false;
};
</script>三、动态插槽名与条件插槽的结合使用
3.1 综合案例:动态表单组件
<!-- DynamicForm.vue -->
<template>
<form class="dynamic-form" @submit.prevent="$emit('submit')">
<!-- 动态渲染表单字段 -->
<div v-for="field in fields" :key="field.name" class="form-field">
<label :for="field.name" class="field-label">
{{ field.label }}
</label>
<!-- 使用动态插槽名渲染自定义字段 -->
<slot :name="field.type" :field="field" :value="formData[field.name]">
<!-- 默认渲染 -->
<input
:id="field.name"
:type="field.type"
v-model="formData[field.name]"
class="field-input"
/>
</slot>
</div>
<!-- 条件渲染:操作按钮区域 -->
<footer v-if="$slots.actions" class="form-actions">
<slot name="actions" />
</footer>
</form>
</template>
<script setup>
import { ref, reactive } from "vue";
const props = defineProps({
fields: {
type: Array,
required: true,
// 期望格式:[{ name: 'username', label: '用户名', type: 'text' }, ...]
},
});
const emits = defineEmits(["submit"]);
// 初始化表单数据
const formData = reactive(
props.fields.reduce((acc, field) => {
acc[field.name] = "";
return acc;
}, {}),
);
// 暴露表单数据供父组件使用
defineExpose({ formData });
</script>
<style scoped>
.dynamic-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.field-label {
font-weight: 500;
color: #333;
}
.field-input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
</style>使用方式:
<template>
<DynamicForm :fields="formFields" @submit="handleSubmit">
<!-- 自定义 email 字段的渲染 -->
<template #email="{ field, value }">
<input
:id="field.name"
type="email"
:value="value"
@input="
$emit('update:modelValue', {
...modelValue,
[field.name]: $event.target.value,
})
"
class="field-input email-input"
placeholder="请输入邮箱地址"
/>
</template>
<!-- 自定义 textarea 字段的渲染 -->
<template #textarea="{ field, value }">
<textarea
:id="field.name"
:value="value"
@input="
$emit('update:modelValue', {
...modelValue,
[field.name]: $event.target.value,
})
"
class="field-input textarea-input"
rows="4"
></textarea>
</template>
<!-- 自定义操作按钮 -->
<template #actions>
<button type="button" class="btn btn-secondary">重置</button>
<button type="submit" class="btn btn-primary">提交</button>
</template>
</DynamicForm>
</template>
<script setup>
import { ref } from "vue";
import DynamicForm from "./DynamicForm.vue";
const formFields = ref([
{ name: "username", label: "用户名", type: "text" },
{ name: "email", label: "邮箱", type: "email" },
{ name: "bio", label: "个人简介", type: "textarea" },
{ name: "age", label: "年龄", type: "number" },
]);
const handleSubmit = () => {
console.log("表单提交");
};
</script>四、课后Quiz
题目1:以下哪种语法可以动态指定插槽名?
A. <template v-slot="slotName">
B. <template v-slot:[slotName]>
C. <template #[slotName]>
D. <template :name="slotName">
答案解析:B、C
v-slot:[slotName] 是完整语法,#[slotName] 是简写形式,两者都可以动态指定插槽名。选项A是作用域插槽的语法,不是动态插槽名。选项D不是有效的Vue语法。
题目2:如何在子组件中检查某个插槽是否存在?
A. this.slots.header
B. this.$slots.header
C. $slots.header
D. slots.header
答案解析:C
在<script setup>中,我们可以直接使用$slots对象来检查插槽是否存在。$slots是Vue提供的模板中可用的属性。
题目3:条件插槽的主要应用场景是什么?
A. 动态改变插槽名 B. 根据插槽是否存在来决定渲染哪些容器或样式 C. 传递数据给插槽 D. 设置插槽的默认内容
答案解析:B
条件插槽的核心用途是根据父组件是否提供了某个插槽,来决定子组件是否渲染对应的容器或应用特定的样式。这可以避免多余的DOM节点和不必要的样式。
五、常见报错解决方案
1. 报错:动态插槽名表达式无效
原因:动态插槽名的表达式受到与动态指令参数相同的语法限制。
错误示例:
<template>
<BaseLayout>
<!-- 错误:表达式包含空格 -->
<template #[slotName + ' ']">内容</template>
<!-- 错误:表达式返回undefined -->
<template #[undefinedSlot]">内容</template>
</BaseLayout>
</template>解决办法:
<template>
<BaseLayout>
<!-- 正确:使用有效的表达式 -->
<template #[`${slotName}-section`]">内容</template>
<!-- 正确:确保表达式返回有效的字符串 -->
<template #[currentSlot || 'default']">内容</template>
</BaseLayout>
</template>2. 报错:条件插槽判断错误
原因:$slots属性返回的是渲染函数,不是布尔值,直接判断可能不符合预期。
解决办法:
<!-- 正确:使用 v-if 检查插槽是否存在 -->
<header v-if="$slots.header">
<slot name="header" />
</header>
<!-- 也可以使用计算属性进行更复杂的判断 -->
<script setup>
import { computed } from "vue";
const hasHeader = computed(() => !!$slots.header);
const hasFooter = computed(() => !!$slots.footer);
</script>3. 预防建议
动态插槽名表达式应该返回有效的字符串
使用
$slots检查插槽时,使用!!转换为布尔值对于复杂的插槽判断逻辑,考虑使用计算属性
为动态插槽提供合理的默认值,避免undefined情况
参考链接:https://cn.vuejs.org/guide/components/slots.html#dynamic-slot-names
余下文章内容请点击跳转至 个人博客页面 或者 扫描关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:
往期文章归档
</details>
免费好用的热门在线工具
评论
发表评论