本篇定位插槽是 Vue 组件化体系中最强大的扩展机制之一它让组件变得高度可定制化。本篇将系统讲解插槽的三种类型——默认插槽、具名插槽和作用域插槽以及它们的高级用法。读完本篇你将能够设计出灵活、可复用、语义清晰的组件 API。一、为什么需要插槽在组件化开发中我们经常会遇到这样的需求组件的大部分结构和行为是固定的但某一部分内容需要由使用者自定义。典型场景卡片组件标题固定内容区域由使用者填充弹窗组件头部、主体、底部都需要自定义列表组件列表结构固定每个列表项的渲染方式由使用者决定没有插槽的解决方案Props 传递内容!-- 卡片组件 -- script setup defineProps{ title: string content: string }() /script template div classcard h3{{ title }}/h3 p{{ content }}/p /div /template !-- 使用 -- Card title标题 content这是一段文本内容 /这种方式的问题内容只能是纯文本或简单 HTML无法传递复杂结构使用者无法控制内容的样式和布局扩展性差无法传入组件或其他复杂内容插槽的解决方案组件内容由父组件通过插槽传入子组件在指定位置渲染这些内容。!-- 卡片组件 -- template div classcard slot nameheader默认标题/slot slot默认内容/slot slot namefooter默认底部/slot /div /template !-- 使用 -- Card template #header h2 classcustom-title自定义标题/h2 /template div classcustom-body p自定义内容区域/p button操作按钮/button /div template #footer span stylecolor: gray;自定义底部/span /template /Card二、默认插槽Default Slot2.1 基本用法默认插槽是最基础的插槽类型它接收没有指定名称的内容!-- 子组件Button.vue -- template button classbtn !-- 默认插槽渲染父组件传入的内容 -- slot默认按钮文字/slot /button /template!-- 父组件 -- script setup import Button from ./Button.vue /script template !-- 传入内容覆盖默认插槽 -- Button点击我/Button !-- 使用原始 HTML -- Buttonstrong重要/strong 按钮/Button !-- 传入其他组件 -- Button span classicon/span 搜索 /Button !-- 不传入内容显示默认值 -- Button / /template渲染结果buttonclassbtn点击我/buttonbuttonclassbtnstrong重要/strong按钮/buttonbuttonclassbtnspanclassicon/span搜索/buttonbuttonclassbtn默认按钮文字/button2.2 插槽的默认内容在slot标签之间可以放置默认内容当父组件没有提供插槽内容时显示template div classcard slot !-- 默认内容 -- p stylecolor: #999;暂无内容请添加/p /slot /div /template2.3 插槽内容与样式插槽内容由父组件控制样式但父组件的样式默认不会污染子组件scoped样式隔离!-- 父组件 -- style scoped /* 这个样式只作用于父组件不会影响插槽内容 */ .parent-class { color: red; } /style template Card !-- 插槽内容的样式由父组件决定 -- div classparent-class这段文字是红色的/div /Card /template如果需要在子组件中控制插槽内容的样式可以使用:deep()或全局样式。三、具名插槽Named Slot3.1 基本用法当组件需要多个插槽来承载不同位置的内容时使用具名插槽!-- 子组件Layout.vue -- template div classlayout header slot nameheader默认头部/slot /header main slot默认主体内容/slot /main footer slot namefooter默认底部/slot /footer /div /template父组件使用v-slot或#简写!-- 父组件 -- script setup import Layout from ./Layout.vue /script template Layout !-- 具名插槽使用 v-slot:name 或 #name -- template #header h1自定义头部/h1 /template !-- 默认插槽无需指定名称 -- p这是主体内容区域/p template #footer span自定义底部/span /template /Layout /template渲染结果divclasslayoutheaderh1自定义头部/h1/headermainp这是主体内容区域/p/mainfooterspan自定义底部/span/footer/div3.2 具名插槽的简写语法v-slot的简写是#这是 Vue 3 推荐的写法!-- 完整写法 -- template v-slot:header h1头部/h1 /template !-- 简写写法 -- template #header h1头部/h1 /template !-- 动态插槽名后面会讲 -- template #[dynamicSlotName] 动态插槽内容 /template3.3 同时使用默认插槽和具名插槽在同一个组件中默认插槽和具名插槽可以共存template div classmodal div classmodal-header slot nameheader h3默认标题/h3 /slot button clickclose×/button /div div classmodal-body slot默认内容/slot /div div classmodal-footer slot namefooter button clickclose关闭/button /slot /div /div /template父组件使用时默认插槽的内容直接写在组件标签内具名插槽使用#Modal !-- 默认插槽直接写 -- p弹窗内容/p !-- 具名插槽使用 # -- template #footer button classbtn-primary确认/button button clickclose取消/button /template /Modal3.4 渲染作用域插槽内容可以访问父组件的数据和作用域但不能直接访问子组件的数据!-- 父组件 -- script setup const parentMessage 来自父组件 /script template Child !-- ✅ 可以访问父组件的数据 -- p{{ parentMessage }}/p !-- ❌ 不能直接访问子组件的数据 -- !-- p{{ childData }}/p -- /Child /template!-- 子组件 -- script setup const childData 来自子组件 /script template div slot/slot /div /template四、作用域插槽Scoped Slot4.1 基本概念作用域插槽允许子组件向父组件传递数据父组件可以根据这些数据来渲染插槽内容。这是插槽最强大的特性。数据流向子组件数据 → (通过插槽传递) → 父组件接收 → 父组件渲染内容4.2 基本用法!-- 子组件TodoList.vue -- script setup import { ref } from vue const todos ref([ { id: 1, text: 学习 Vue 插槽, completed: false }, { id: 2, text: 完成项目, completed: true }, { id: 3, text: 阅读文档, completed: false } ]) /script template ul classtodo-list li v-fortodo in todos :keytodo.id !-- 通过插槽传递数据使用 v-bind 绑定 -- slot :todotodo :indexindex !-- 默认渲染方式 -- span :class{ completed: todo.completed } {{ todo.text }} /span /slot /li /ul /template!-- 父组件 -- script setup import TodoList from ./TodoList.vue /script template TodoList !-- v-slot:default 接收数据 -- template #default{ todo, index } div classcustom-item span classindex{{ index 1 }}./span span :class{ done: todo.completed } {{ todo.text }} /span span classstatus {{ todo.completed ? ✅ : ⏳ }} /span /div /template /TodoList /template关键语法子组件通过slot :propNamedata传递数据父组件通过v-slot:default{ propName }或#default{ propName }接收4.3 具名作用域插槽作用域插槽也可以有名称!-- 子组件 -- template div slot nameheader :useruser :countcount 默认头部 /slot slot :itemsitems默认列表/slot slot namefooter :datedate 默认底部 /slot /div /template!-- 父组件 -- template ChildComponent !-- 具名作用域插槽 -- template #header{ user, count } h2用户{{ user.name }}{{ count }} 项/h2 /template !-- 默认作用域插槽 -- template #default{ items } ul li v-foritem in items :keyitem.id {{ item.text }} /li /ul /template template #footer{ date } p更新于{{ date }}/p /template /ChildComponent /template4.4 解构插槽数据作用域插槽的数据可以使用解构赋值来提取!-- 基础解构 -- template #default{ todo, index } span{{ index }}: {{ todo.text }}/span /template !-- 重命名 -- template #default{ todo: item, index: position } span{{ position }}: {{ item.text }}/span /template !-- 默认值 -- template #default{ todo { text: 默认任务 } } span{{ todo.text }}/span /template4.5 使用场景可定制列表作用域插槽最经典的场景是可定制列表组件!-- DataList.vue - 通用列表组件 -- script setup import { ref } from vue interface Props { items: any[] loading?: boolean emptyText?: string } const props withDefaults(definePropsProps(), { loading: false, emptyText: 暂无数据 }) /script template div classdata-list div v-ifloading classloading加载中.../div div v-else-ifitems.length 0 classempty {{ emptyText }} /div div v-else classlist-container !-- 让父组件决定每个项的渲染方式 -- div v-for(item, index) in items :keyindex classlist-item slot :itemitem :indexindex !-- 默认渲染显示 JSON -- pre{{ item }}/pre /slot /div /div /div /template!-- 父组件使用 -- script setup import DataList from ./DataList.vue import { ref } from vue const users ref([ { id: 1, name: 张三, age: 25, avatar: }, { id: 2, name: 李四, age: 30, avatar: }, { id: 3, name: 王五, age: 28, avatar: } ]) const products ref([ { id: 1, name: 笔记本电脑, price: 5999, inStock: true }, { id: 2, name: 手机, price: 3999, inStock: false } ]) /script template !-- 用户列表自定义卡片样式 -- DataList :itemsusers template #default{ item, index } div classuser-card span classavatar{{ item.avatar }}/span div classinfo h4{{ item.name }}/h4 p年龄{{ item.age }} 岁/p /div span classindex-badge#{{ index 1 }}/span /div /template /DataList !-- 商品列表不同的自定义样式 -- DataList :itemsproducts empty-text暂无商品 template #default{ item } div classproduct-item span classproduct-name{{ item.name }}/span span classproduct-price¥{{ item.price.toFixed(2) }}/span span classproduct-stock :class{ out: !item.inStock } {{ item.inStock ? ✅ 有货 : ❌ 缺货 }} /span /div /template /DataList /template4.6 v-for 与插槽结合的最佳实践当在v-for中使用插槽时注意为插槽传递正确的数据!-- 子组件 -- template div v-for(item, index) in items :keyitem.id !-- 传递 item 和 index -- slot nameitem :itemitem :indexindex {{ item.text }} /slot /div /template !-- 父组件 -- template List :itemslist template #item{ item, index } !-- 每个项都有自己的作用域 -- div classitem span{{ index 1 }}/span span{{ item.name }}/span /div /template /List /template五、动态插槽名5.1 基本用法Vue 3 支持使用动态表达式作为插槽名称!-- 父组件 -- script setup import { ref } from vue const slotName ref(header) const dynamicSlots ref([header, body, footer]) /script template Child !-- 动态插槽名 -- template #[slotName] 动态插槽内容 /template !-- 使用表达式 -- template #[dynamicSlots[0]] 第一个插槽 /template !-- 条件插槽 -- template v-ifshowExtraSlot #[extraSlotName] 条件插槽 /template /Child /template5.2 实际应用场景场景一动态布局!-- 父组件 -- script setup import { ref } from vue const layout ref(default) const layouts { default: [header, body, footer], compact: [header, body], minimal: [body] } function switchLayout(type) { layout.value type } /script template div button clickswitchLayout(default)默认布局/button button clickswitchLayout(compact)紧凑布局/button button clickswitchLayout(minimal)极简布局/button DynamicLayout template v-forslotName in layouts[layout] :keyslotName #[slotName] {{ slotName }} 区域的内容 /template /DynamicLayout /div /template场景二可扩展的组件系统!-- 组件库中的可扩展组件 -- template BaseComponent !-- 动态渲染所有自定义插槽 -- template v-for(_, slotName) in $slots :keyslotName #[slotName] slot :nameslotName / /template /BaseComponent /template六、插槽的高级模式6.1 插槽的透传Transparent Slot将父组件传入的插槽原样传递给子组件!-- 包装组件Wrapper.vue -- template div classwrapper !-- 透传所有插槽到子组件 -- Child !-- 使用 $slots 动态传递 -- template v-for(_, slotName) in $slots :keyslotName #[slotName] slot :nameslotName / /template /Child /div /template6.2 插槽的多次使用同一个插槽内容可以在子组件中多次渲染!-- 子组件Banner.vue -- template div classbanner !-- 在顶部渲染一次 -- div classbanner-top slot namecontent / /div !-- 在底部再渲染一次 -- div classbanner-bottom slot namecontent / /div /div /template6.3 条件插槽渲染根据插槽是否存在来决定是否渲染某个区域!-- 子组件Card.vue -- script setup import { useSlots } from vue const slots useSlots() const hasHeader !!slots.header const hasFooter !!slots.footer /script template div classcard div v-ifhasHeader classcard-header slot nameheader / /div div classcard-body slot / /div div v-ifhasFooter classcard-footer slot namefooter / /div /div /template6.4 使用 $slots 获取插槽信息$slots是一个对象包含了所有传入的插槽script setup import { useSlots } from vue const slots useSlots() // 检查插槽是否存在 console.log(slots.header) // 函数 | undefined console.log(slots.default) // 函数 | undefined // 获取插槽内容 if (slots.header) { const headerContent slots.header() console.log(headerContent) // VNode 数组 } /script template div !-- 根据是否有默认插槽渲染不同样式 -- div v-if$slots.default classhas-content slot / /div div v-else classempty 没有内容 /div /div /template6.5 插槽内容与作用域插槽内容中的变量查找顺序父组件作用域 → 全局作用域不会访问子组件作用域!-- 父组件 -- script setup const message 父组件的消息 const sameName 父组件 /script template Child !-- ✅ 使用父组件的变量 -- p{{ message }}/p !-- ✅ 使用父组件的变量子组件中也有 sameName但优先使用父组件的 -- p{{ sameName }}/p /Child /template!-- 子组件 -- script setup const sameName 子组件 /script template div slot/slot !-- 子组件自己的变量 -- p子组件{{ sameName }}/p /div /template七、综合实战可定制表格组件让我们综合运用所有插槽知识构建一个功能完整的可定制表格组件!-- DataTable.vue - 通用数据表格 -- script setup langts import { useSlots, computed } from vue interface Column { key: string label: string width?: string | number align?: left | center | right sortable?: boolean } interface Props { columns: Column[] data: Recordstring, any[] loading?: boolean emptyText?: string striped?: boolean hoverable?: boolean } const props withDefaults(definePropsProps(), { loading: false, emptyText: 暂无数据, striped: true, hoverable: true }) const slots useSlots() // 检查是否有某个列的作用域插槽 function hasColumnSlot(key: string): boolean { return !!slots[column-${key}] } // 排序状态 const sortKey refstring | null(null) const sortOrder refasc | desc(asc) function handleSort(key: string) { if (!props.columns.find(c c.key key)?.sortable) return if (sortKey.value key) { sortOrder.value sortOrder.value asc ? desc : asc } else { sortKey.value key sortOrder.value asc } } // 排序后的数据 const sortedData computed(() { if (!sortKey.value) return props.data const key sortKey.value const order sortOrder.value return [...props.data].sort((a, b) { const aVal a[key] const bVal b[key] if (aVal bVal) return 0 const result aVal bVal ? 1 : -1 return order asc ? result : -result }) }) // 触发事件 const emit defineEmits{ (e: row-click, row: Recordstring, any, index: number): void (e: sort-change, key: string, order: asc | desc): void }() function onRowClick(row: Recordstring, any, index: number) { emit(row-click, row, index) } /script template div classdata-table !-- 表格标题插槽 -- div v-if$slots.header classtable-header slot nameheader / /div !-- 加载状态 -- div v-ifloading classtable-loading span classspinner/span p加载中.../p /div !-- 空状态 -- div v-else-ifdata.length 0 classtable-empty slot nameempty p{{ emptyText }}/p /slot /div !-- 表格 -- table v-else classtable thead tr th v-forcol in columns :keycol.key :style{ width: col.width, textAlign: col.align || left } :class{ sortable: col.sortable, sorted-asc: sortKey col.key sortOrder asc, sorted-desc: sortKey col.key sortOrder desc } clickhandleSort(col.key) span classth-content {{ col.label }} span v-ifcol.sortable classsort-icon span v-ifsortKey col.key {{ sortOrder asc ? ▲ : ▼ }} /span span v-else⇅/span /span /span /th /tr /thead tbody tr v-for(row, rowIndex) in sortedData :keyrowIndex :class{ striped: striped rowIndex % 2 1, hoverable: hoverable } clickonRowClick(row, rowIndex) td v-forcol in columns :keycol.key :style{ textAlign: col.align || left } !-- 列作用域插槽 -- slot :namecolumn-${col.key} :rowrow :indexrowIndex :valuerow[col.key] !-- 默认渲染 -- template v-iftypeof row[col.key] boolean span :classrow[col.key] ? badge-success : badge-danger {{ row[col.key] ? 是 : 否 }} /span /template template v-else {{ row[col.key] ?? - }} /template /slot /td /tr /tbody /table !-- 底部插槽 -- div v-if$slots.footer classtable-footer slot namefooter :totaldata.length / /div /div /template style scoped .data-table { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.06); } .table-header { padding: 16px 20px; border-bottom: 1px solid #e2e8f0; font-weight: 600; } .table { width: 100%; border-collapse: collapse; font-size: 14px; } .table thead { background: #f7fafc; border-bottom: 2px solid #e2e8f0; } .table th { padding: 12px 16px; text-align: left; font-weight: 600; color: #4a5568; user-select: none; transition: background 0.2s; } .table th.sortable { cursor: pointer; } .table th.sortable:hover { background: #edf2f7; } .th-content { display: inline-flex; align-items: center; gap: 6px; } .sort-icon { font-size: 12px; opacity: 0.5; } .sorted-asc .sort-icon, .sorted-desc .sort-icon { opacity: 1; color: #4299e1; } .table td { padding: 12px 16px; border-bottom: 1px solid #edf2f7; color: #2d3748; } .table tbody tr { transition: background 0.15s; cursor: default; } .table tbody tr.striped { background: #fafbfc; } .table tbody tr.hoverable:hover { background: #ebf8ff; } .badge-success { color: #276749; background: #f0fff4; padding: 2px 10px; border-radius: 12px; font-size: 12px; } .badge-danger { color: #9b2c2c; background: #fff5f5; padding: 2px 10px; border-radius: 12px; font-size: 12px; } .table-loading { padding: 60px 20px; text-align: center; color: #a0aec0; } .spinner { display: inline-block; width: 32px; height: 32px; border: 4px solid #e2e8f0; border-top-color: #4299e1; border-radius: 50%; animation: spin 0.8s linear infinite; } keyframes spin { to { transform: rotate(360deg); } } .table-empty { padding: 60px 20px; text-align: center; color: #a0aec0; } .table-footer { padding: 12px 20px; border-top: 1px solid #e2e8f0; font-size: 13px; color: #718096; } /style使用数据表格组件script setup import { ref } from vue import DataTable from ./DataTable.vue const users ref([ { id: 1, name: 张三, age: 25, email: zhangsanexample.com, active: true }, { id: 2, name: 李四, age: 30, email: lisiexample.com, active: false }, { id: 3, name: 王五, age: 28, email: wangwuexample.com, active: true } ]) const columns [ { key: id, label: ID, width: 60, align: center, sortable: true }, { key: name, label: 姓名, sortable: true }, { key: age, label: 年龄, width: 80, align: center, sortable: true }, { key: email, label: 邮箱 }, { key: active, label: 状态, width: 80, align: center } ] const loading ref(false) function onRowClick(row: any) { console.log(点击行, row) } function toggleLoading() { loading.value true setTimeout(() { loading.value false }, 2000) } /script template div classapp h2 用户管理/h2 button clicktoggleLoading切换加载/button DataTable :columnscolumns :datausers :loadingloading empty-text暂无用户数据 row-clickonRowClick !-- 表格头部插槽 -- template #header div styledisplay: flex; justify-content: space-between; span用户列表/span span stylefont-weight: 400; color: #718096; 共 {{ users.length }} 个用户 /span /div /template !-- 自定义列姓名列显示为链接 -- template #column-name{ row } a href# stylecolor: #4299e1; text-decoration: none; {{ row.name }} /a /template !-- 自定义列状态列使用自定义样式 -- template #column-active{ row } span :style{ color: row.active ? #48bb78 : #fc8181, fontWeight: 600 } {{ row.active ? ✅ 在线 : ❌ 离线 }} /span /template !-- 表格底部插槽 -- template #footer{ total } span总计 {{ total }} 条记录/span span stylemargin-left: 20px; 点击行可查看详情 /span /template /DataTable /div /template八、本篇小结知识点核心内容关键要点默认插槽slot标签接收无名称内容支持默认内容具名插槽slot namexxxtemplate #xxx组件多个位置的内容定制作用域插槽子组件向父组件传递数据v-slot{ prop }接收数据动态插槽名#[dynamicName]动态决定插槽名称插槽检测useSlots()或$slots判断插槽是否存在插槽透传使用$slots遍历传递包装组件中使用九、思考与练习基础练习创建一个Collapsible组件包含标题区域#header和内容区域默认插槽实现点击标题切换展开/收起。实践任务使用作用域插槽创建一个Select组件允许父组件自定义下拉选项的渲染方式文本、图标文本、带描述的选项等。陷阱排查以下代码有什么问题!-- 子组件 -- template div slot nameitem :dataitemData / /div /template !-- 父组件 -- template Child !-- 这里想使用子组件传递的 data -- template #item span{{ data.name }}/span /template /Child /template深度思考作用域插槽和 Props 都可以从子组件向父组件传递数据它们的核心区别是什么各自适合什么场景扩展任务在数据表格组件中增加分页功能使用插槽让父组件自定义分页器的渲染方式。下篇预告第 11 篇我们将深入 Vue 3 的核心——组合式 API系统学习setup函数的执行时机与参数script setup语法糖的全面解析响应式 APIref、reactive、computed、watch等的使用组合式 API 与选项式 API 的对比与选择准备好迎接 Vue 3 最强大的特性⚡