Vue 3 动态插槽名与条件插槽的灵活应用完全指南


发现1000+提升效率与开发的AI工具和实用程序https://tools.cmdragon.cn/

一、动态插槽名:运行时决定内容去向

1.1 什么是动态插槽名?

在之前的章节中,我们学习了如何使用固定的插槽名来指定内容渲染的位置。但Vue还提供了更强大的功能:动态插槽名。这意味着我们可以在运行时根据组件的状态或props的值,动态决定内容应该渲染到哪个插槽。

动态插槽名使用动态指令参数的语法: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"
     >
       {{ tab.label }}
     </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>{{ sectionIndex }} 节内容</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

余下文章内容请点击跳转至 个人博客页面 或者 扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:Vue 3 动态插槽名与条件插槽的灵活应用完全指南

往期文章归档

</details>

免费好用的热门在线工具

</details>

评论

此博客中的热门博文

数据库的创建与删除:理论与实践

深入探讨聚合函数(COUNT, SUM, AVG, MAX, MIN):分析和总结数据的新视野

数据库与编程语言的连接