先记住这个答案
子组件通过 <slot name="header">、<slot name="actions"> 定义具名出口,没有 name 的出口名称是 default。父级用 <template v-slot:header> 提供内容,简写为 <template #header>;默认插槽可以写 #default。名称也支持动态参数,例如 #[activeSlot],但表达式必须满足动态指令参数限制,并且运行时名称要和子组件出口完全匹配。v-slot 应写在组件标签或直接包裹插槽内容的 template 上,不能随意放到普通 HTML 元素。
- slot 的 name 定义通道
- v-slot:name 可简写为 #name
- 动态插槽名在运行时匹配出口
出口名称定义布局角色
子组件负责卡片骨架,父级决定每个区域的实际节点。具名插槽比传入大段 HTML 字符串安全,也比为每种按钮组合增加 prop 更灵活。默认插槽保留最常见主体,让调用方不必给唯一主要内容重复命名。
名称应描述结构角色而不是当前样式,例如 actions 比 blueButtons 更能承受设计变化。组件还可以为某个具名出口提供后备节点,未提供时保持完整布局或直接省略区域。
模板语法与渲染函数契约一致
下例 CardShell 在渲染函数中调用 header、default 和 actions 三个插槽,父级以同名函数提供节点。模板版本对应 #header、默认内容和 #actions。
插槽函数可能不存在,因此库组件调用前要判空;也可以用可选调用或后备数组。不要在 setup 顶层调用后缓存结果,应该让每轮渲染取得当前父级状态。
import { defineComponent, h } from 'vue'
const CardShell = defineComponent({
setup(_props, { slots }) {
return () => h('article', [
h('header', slots.header?.() ?? [h('h2', '默认标题')]),
h('main', slots.default?.()),
h('footer', slots.actions?.())
])
}
})
const ProfileCard = () => h(CardShell, null, {
header: () => h('h2', '个人资料'),
default: () => h('p', '主体'),
actions: () => h('button', '保存')
})三个函数名对应三个出口。若省略 header,组件显示默认标题;省略 actions 时 footer 没有子节点,是否连 footer 一起隐藏应由组件布局契约决定。
动态名称适合配置化区域
<template #[activeSlot]> 可以把一份内容投向运行时选定的出口,常用于表格单元格或配置驱动布局。它增加了静态分析难度,拼写错误不会自动找到最相近名称,因此应把允许名称收窄成联合类型或配置枚举。
动态名称不是用来绕过稳定 API 的万能入口。若每个消费者都拼接任意 slot 名,组件升级难以发现破坏性变化;大型组件可以导出允许的列键或在测试中遍历配置确认每个出口存在。
容易答错的地方
- 把 # 当作独立属性名使用
- #header 是 v-slot:header 的简写,必须带名称;只有存在默认参数时才写 #default,不能只写一个裸 #。
- 在普通 div 上使用 v-slot
- v-slot 面向组件提供的插槽或 template 包装器。普通元素没有组件插槽上下文,语法位置错误会导致编译提示或内容不按预期归类。
面试官还会怎么问?
默认插槽为什么有时要显式写 #default?
当同一组件同时使用多个具名 template,且默认内容也需要接收 slot props 或避免作用域歧义时,显式 #default 更清楚。
具名插槽能传参数吗?
能。子组件在对应 <slot name="header" :item="item"> 上传出 props,父级在 #header="{ item }" 中接收;名称选择和数据传递是两个独立维度。
怎样判断 actions 插槽是否提供?
setup 中通过 slots.actions 是否存在判断,模板中可检查 $slots.actions。若函数存在但条件渲染为空,是否隐藏容器还需更明确的接口。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。