Vue 3 深入学习笔记 08:插槽(Slots)与组件的内容扩展设计

发布时间:2026/10/19 4:10:09

Vue 3 深入学习笔记 08:插槽(Slots)与组件的内容扩展设计 Vue 3 深入学习笔记 08插槽Slots与组件的内容扩展设计一、为什么需要学习插槽在前面第 06 篇中我们学习了组件设计与职责边界第 07 篇学习了v-model与受控组件设计。现在我们继续思考一个真实的业务问题如果一个组件的外部结构固定但是内部内容需要根据业务场景变化应该怎么设计例如我们需要开发一个通用的弹窗组件。不同业务使用这个弹窗时内容可能完全不同登录弹窗显示用户名、密码输入框。删除确认弹窗显示删除提示。订单详情弹窗显示订单信息。会员升级弹窗显示会员权益和升级按钮。如果不使用插槽可能会写成这样template div classdialog h3{{ title }}/h3 div v-iftype login !-- 登录表单 -- /div div v-else-iftype delete !-- 删除提示 -- /div div v-else-iftype order !-- 订单详情 -- /div /div /template这种设计存在几个问题每增加一种业务场景就需要修改弹窗组件。弹窗组件逐渐承担越来越多的业务职责。组件与具体业务强耦合复用性越来越差。如果业务内容特别复杂组件内部就会充满v-if、v-else-if。我们真正希望实现的是弹窗组件负责弹窗的结构和行为具体显示什么内容由使用它的父组件决定。这就是插槽的核心价值。二、什么是插槽插槽Slot可以理解为子组件预留一个内容位置由父组件决定向这个位置传入什么内容。先看一个最简单的例子。2.1 不使用插槽子组件MyButton.vuetemplate button提交/button /template父组件template MyButton / /template最终显示提交问题是如果其他页面希望按钮显示“取消”怎么办如果按钮文字写死在子组件里就只能修改子组件或者额外设计一个text属性。2.2 使用插槽子组件MyButton.vuetemplate button slot/slot /button /template父组件template MyButton 提交 /MyButton MyButton 取消 /MyButton MyButton 保存 /MyButton /template这里的slot/slot就是插槽。父组件传入的内容会出现在子组件slot所在的位置。最终相当于button提交/button button取消/button button保存/button注意这里展示的是便于理解的最终 DOM 结构Vue 实际还会处理组件渲染等细节。2.3 插槽和 props 有什么区别这是本篇最重要的问题之一。props主要用于向子组件传递数据插槽主要用于向子组件传递内容结构。例如MyButton text提交 /使用propsscript setup defineProps({ text: String }) /script template button{{ text }}/button /template这种方式适合简单、固定类型的数据例如文字、颜色、尺寸、状态等。而使用插槽MyButton span提交/span strong订单/strong /MyButton父组件可以传入多个元素甚至传入其他组件。对比项propsslots主要用途传递数据和配置传递内容结构典型内容文字、数字、布尔值、对象HTML 元素、组件及其组合灵活性由子组件定义数据接口父组件可以自由组合内容适合场景按钮尺寸、标题、加载状态弹窗内容、卡片主体、表格单元格这里并不是说props不能控制内容或者插槽不能接收数据而是两者的设计职责不同。例如一个弹窗的标题通常适合用props传入弹窗主体则通常适合使用插槽。三、默认插槽Default Slot默认插槽是最常见的插槽类型。3.1 基本用法子组件BaseCard.vuetemplate div classcard slot/slot /div /template父组件template BaseCard h3订单信息/h3 p订单编号10001/p p订单金额500 元/p /BaseCard /template最终结构可以理解为div classcard h3订单信息/h3 p订单编号10001/p p订单金额500 元/p /divBaseCard只负责卡片容器的结构和样式内部展示哪些元素由父组件决定。这是一种很常见的通用组件设计方式。3.2 插槽的默认内容插槽还可以提供默认内容。子组件template button slot默认按钮/slot /button /template父组件template BaseButton / BaseButton保存/BaseButton /template结果第一个按钮显示“默认按钮”。第二个按钮显示“保存”。只有父组件没有提供有效的插槽内容时才会使用插槽中的默认内容。3.3 实际开发中如何理解插槽默认内容适合用于提供合理的兜底显示。例如template div classempty-state slot 暂无数据 /slot /div /template调用方可以直接使用默认提示也可以自定义空状态内容EmptyState img src/empty.png alt暂无数据 / p当前没有订单/p /EmptyState这样通用组件不必知道具体业务需要展示什么空状态。四、具名插槽Named Slots前面的例子只有一个插槽。但是假设我们设计一个弹窗组件它有三个区域标题区域。主体内容区域。底部按钮区域。如果只有一个默认插槽父组件就很难分别控制这三个区域。这时候就需要具名插槽。4.1 子组件定义多个插槽BaseDialog.vuetemplate div classdialog header classdialog-header slot nameheader 默认标题 /slot /header main classdialog-body slot/slot /main footer classdialog-footer slot namefooter/slot /footer /div /template这里有三个插槽插槽名称用途header标题区域默认插槽主体内容区域footer底部操作区域4.2 父组件指定插槽内容template BaseDialog template #header h3删除订单/h3 /template p确定要删除这笔订单吗/p template #footer button取消/button button确认删除/button /template /BaseDialog /template这里的#header是v-slot:header的简写。例如template #header h3删除订单/h3 /template等价于template v-slot:header h3删除订单/h3 /template最终效果是标题显示在header插槽的位置。删除提示显示在默认插槽的位置。两个操作按钮显示在footer插槽的位置。具名插槽的本质是让父组件分别向子组件的不同内容区域提供内容。4.3 为什么不能直接随便写一个元素例如BaseDialog h3删除订单/h3 p确定要删除吗/p /BaseDialog这两个元素都会进入默认插槽而不会自动进入标题区域。插槽的分配依据是插槽名称而不是内容看起来像什么。4.4 具名插槽的设计价值实际项目中很多组件都具有相对固定的区域结构。例如BaseCard ├── header ├── default └── footerBaseDialog ├── header ├── default └── footerBaseLayout ├── sidebar ├── header ├── default └── footer这些组件可以规定页面结构但不需要规定每个区域具体展示什么内容。这种设计特别适合通用 UI 组件库。五、作用域插槽Scoped Slots接下来进入本篇最值得深入理解的知识点。默认插槽和具名插槽解决了内容扩展的问题但还存在一个问题如果父组件传入的内容需要使用子组件内部的数据怎么办例如子组件内部维护了一组商品数据希望父组件决定每个商品如何展示。这时候就需要作用域插槽。5.1 先理解一个重要规则Vue 中父组件模板中的表达式默认在父组件的作用域中解析子组件模板中的表达式则在子组件的作用域中解析。看一个例子。子组件ProductList.vuescript setup import { ref } from vue const products ref([ { id: 1, name: 键盘, price: 200 }, { id: 2, name: 鼠标, price: 100 } ]) /script template ul li v-forproduct in products :keyproduct.id {{ product.name }} /li /ul /template父组件script setup const title 商品列表 /script template ProductList {{ title }} /ProductList /template这里有一个容易产生的误解父组件把title放在了ProductList标签内部就认为它可以直接访问子组件的products。实际上并不是这样。插槽内容属于父组件的模板作用域不能因为它写在子组件标签里面就自动访问子组件内部的变量。如果父组件希望根据子组件内部的product数据自定义商品的展示方式就需要让子组件通过插槽向父组件提供数据。5.2 子组件向插槽暴露数据修改ProductList.vuescript setup import { ref } from vue const products ref([ { id: 1, name: 键盘, price: 200 }, { id: 2, name: 鼠标, price: 100 } ]) /script template ul li v-forproduct in products :keyproduct.id slot :productproduct/slot /li /ul /template重点看这一行slot :productproduct/slot它的含义是子组件将当前商品对象通过插槽参数product暴露给父组件。注意:productproduct中左侧product是插槽参数的名称。右侧product是子组件当前作用域中的变量。5.3 父组件接收插槽数据父组件template ProductList template #default{ product } {{ product.name }}{{ product.price }} 元 /template /ProductList /template这里的template #default{ product }表示接收默认插槽提供的数据并从中解构出product。最终显示键盘200 元 鼠标100 元现在父组件可以自由决定商品的展示形式。例如只显示名称ProductList template #default{ product } {{ product.name }} /template /ProductList或者显示名称和价格ProductList template #default{ product } strong{{ product.name }}/strong span价格{{ product.price }} 元/span /template /ProductList或者根据价格决定展示样式ProductList template #default{ product } strong{{ product.name }}/strong span v-ifproduct.price 150 高价商品 /span span v-else 普通商品 /span /template /ProductList子组件负责维护数据、遍历数据和提供展示位置父组件负责决定每一项如何展示。5.4 作用域插槽的数据流可以将其理解为子组件拥有数据 ↓ 子组件通过 slot 参数暴露数据 ↓ 父组件接收插槽参数 ↓ 父组件决定如何展示数据这与props的数据传递方向不同。普通的props是父组件向子组件传递数据而作用域插槽可以让子组件向父组件提供数据供父组件定义插槽内容时使用。这里需要注意这并不意味着父组件获得了子组件的全部内部状态。子组件只暴露了它明确传出的插槽参数。六、作用域插槽的真实业务应用通用表格在实际业务项目中作用域插槽的价值非常大。例如一个通用表格组件需要展示订单列表。不同页面的要求可能不同订单管理页面展示订单金额。售后页面展示售后状态。财务页面展示结算状态。管理页面需要增加操作按钮。如果每种展示方式都写进表格组件表格组件就会不断增加业务判断。我们可以利用作用域插槽解决这个问题。6.1 通用表格组件BaseTable.vuescript setup defineProps({ rows: { type: Array, required: true } }) /script template table tbody tr v-forrow in rows :keyrow.id td slot namedefault :rowrow {{ row.name }} /slot /td /tr /tbody /table /template这个组件只负责接收表格数据。遍历每一行。提供每一行的插槽内容位置。提供一个默认展示方案。它不关心当前页面到底是订单管理、售后管理还是财务管理。6.2 页面 A展示订单金额父组件script setup import { ref } from vue const orders ref([ { id: 1, name: 订单 A, amount: 200 }, { id: 2, name: 订单 B, amount: 500 } ]) /script template BaseTable :rowsorders template #default{ row } span {{ row.name }} {{ row.amount }} 元 /span /template /BaseTable /template6.3 页面 B展示操作按钮同样使用BaseTabletemplate BaseTable :rowsorders template #default{ row } span{{ row.name }}/span button clickviewOrder(row.id) 查看 /button button clickcancelOrder(row.id) 取消订单 /button /template /BaseTable /template这里的viewOrder和cancelOrder由父组件定义。父组件可以根据自己的业务需求决定显示哪些按钮以及点击后执行什么操作。表格组件不需要知道这些业务行为。6.4 这个设计好在哪里它把两个变化相对独立的部分分离了表格结构和数据遍历由通用组件负责。每一行的具体展示和业务操作由调用方负责。当业务新增一种展示方式时通常只需要修改父组件不需要修改通用表格。不过实际表格通常还需要支持列配置、表头、排序、分页等能力。这里的示例只是为了说明作用域插槽的核心思想。七、插槽与组件设计职责边界学习插槽不能只停留在语法层面更重要的是理解它在组件设计中的作用。我们比较两种设计。7.1 设计一组件内部包含所有业务判断template div classcard div v-iftype order !-- 订单内容 -- /div div v-else-iftype user !-- 用户内容 -- /div div v-else-iftype product !-- 商品内容 -- /div /div /template问题通用组件必须理解所有业务类型。新增业务类型时需要修改通用组件。业务逻辑与 UI 结构混杂。测试范围逐渐扩大。7.2 设计二组件提供插槽子组件template div classcard slot/slot /div /template父组件template BaseCard OrderDetail / /BaseCard BaseCard UserDetail / /BaseCard BaseCard ProductDetail / /BaseCard /template这样BaseCard不需要知道订单、用户、商品的具体业务内容。它只负责卡片容器。7.3 什么时候使用 props什么时候使用插槽可以参考以下判断方式。适合使用 props 的情况传递简单配置。控制组件状态或外观。子组件可以根据固定的数据接口完成渲染。例如BaseButton text保存 sizelarge disabled /适合使用插槽的情况需要自定义一块内容区域。需要传入多个元素或其他组件。不同业务场景需要不同的展示结构。希望通用组件不依赖具体业务内容。例如BaseDialog OrderDetail / /BaseDialog适合使用作用域插槽的情况子组件掌握数据。父组件需要决定数据的展示方式。希望复用数据处理逻辑同时允许不同页面自定义 UI。例如ProductList template #default{ product } {{ product.name }}{{ product.price }} /template /ProductList需要注意插槽并不是越多越好。如果组件只有一个简单标题使用title属性通常比设计一个标题插槽更直接。八、插槽与 props、emit 的关系我们已经学习了props、emit、v-model现在把它们与插槽放在一起理解。一个成熟的业务组件通常需要解决三个不同的问题机制主要解决的问题方向或作用props父组件如何向子组件传递数据父 → 子emit子组件如何通知父组件发生了某件事子 → 父slots父组件如何自定义子组件内部的内容结构父定义内容子提供位置作用域插槽父组件如何使用子组件提供的数据自定义内容子提供插槽参数父定义展示例如设计一个通用弹窗script setup defineProps({ title: String, visible: Boolean }) const emit defineEmits([ close, confirm ]) /script template div v-ifvisible classdialog header h3{{ title }}/h3 /header main slot/slot /main footer slot namefooter button clickemit(close) 关闭 /button /slot /footer /div /template调用方template BaseDialog title删除订单 :visiblevisible closevisible false confirmdeleteOrder p确定要删除这笔订单吗/p template #footer button clickvisible false 取消 /button button clickdeleteOrder 确认删除 /button /template /BaseDialog /template这个示例展示了几种机制的分工title和visible使用props传递数据。close和confirm通过事件通知父组件。默认插槽允许父组件自定义弹窗主体。footer插槽允许父组件自定义底部按钮。不过这段代码中声明了confirm事件却没有在子组件模板中主动触发它父组件示例直接处理了自己的删除按钮。实际开发时应根据设计选择由弹窗组件触发confirm还是由父组件的自定义按钮直接执行操作不要保留没有实际用途的事件接口。九、插槽的常见误区9.1 误区一认为插槽内容属于子组件作用域错误理解ProductList {{ products }} /ProductList认为这里的products会自动指向子组件内部的变量。实际上插槽内容中的表达式默认属于父组件的作用域。如果父组件自身没有定义products就不能通过这种方式直接使用子组件的变量。需要通过作用域插槽显式传递ProductList template #default{ product } {{ product.name }} /template /ProductList9.2 误区二把所有内容都设计成插槽例如一个按钮组件只有文字不同BaseButton text保存 /如果为了显示“保存”就设计一套复杂的插槽接口反而会增加使用成本。如果变化只是简单文本使用props往往更合适。如果按钮需要图标、文字、加载动画等复杂组合才更有理由使用插槽。9.3 误区三把作用域插槽当作子组件的数据修改入口作用域插槽的主要用途是暴露数据供父组件展示并不意味着父组件应该随意修改子组件内部状态。例如slot :productproduct/slot父组件拿到product后最好按照明确的数据流约定处理修改。如果需要修改商品通常应设计明确的事件、v-model或由父组件管理数据而不是依赖插槽参数进行隐式修改。9.4 误区四插槽越灵活组件设计就越好插槽可以提升组件的扩展能力但过多、过细的插槽也可能让组件接口变得复杂。例如一个简单的信息提示组件如果设计了十几个插槽使用者可能反而不知道应该在哪个位置传入内容。设计组件时应优先明确哪些结构应该固定。哪些内容需要由调用方决定。哪些数据应该通过props提供。哪些行为需要通过事件通知。然后再决定插槽的数量和粒度。十、课后练习练习一默认插槽设计一个BasePanel.vue要求外层使用div。固定显示一个标题“面板”。主体内容由父组件传入。如果父组件没有传入内容显示“暂无内容”。练习二具名插槽设计一个BaseDialog.vue要求提供header插槽。提供默认插槽作为主体。提供footer插槽。标题插槽没有内容时默认显示“提示”。练习三作用域插槽设计一个UserList.vue内部维护以下数据[ { id: 1, name: 张三, age: 20 }, { id: 2, name: 李四, age: 25 } ]要求子组件负责遍历用户。子组件通过插槽提供当前用户数据。父组件自行决定展示用户名和年龄。练习四组件设计题你正在开发一个通用的商品卡片组件。商品卡片包含商品图片。商品名称。商品价格。购买按钮。请思考哪些内容适合通过props传递哪些内容适合使用插槽如果某个页面需要显示“促销标签”另一个页面需要显示“库存状态”应该怎么设计如果购买按钮的行为因页面而异应该如何处理十一、课后练习答案练习一参考答案BasePanel.vuetemplate div classpanel h3面板/h3 div classpanel-content slot暂无内容/slot /div /div /template父组件template BasePanel p这里是自定义内容。/p /BasePanel BasePanel / /template第一个组件显示自定义内容第二个组件显示“暂无内容”。练习二参考答案BaseDialog.vuetemplate div classdialog header slot nameheader 提示 /slot /header main slot/slot /main footer slot namefooter/slot /footer /div /template父组件template BaseDialog template #header h3删除确认/h3 /template p确定要删除吗/p template #footer button取消/button button确认/button /template /BaseDialog /template这里使用具名插槽分别控制标题和底部内容默认插槽负责主体内容。练习三参考答案UserList.vuescript setup import { ref } from vue const users ref([ { id: 1, name: 张三, age: 20 }, { id: 2, name: 李四, age: 25 } ]) /script template ul li v-foruser in users :keyuser.id slot :useruser {{ user.name }} /slot /li /ul /template父组件template UserList template #default{ user } strong{{ user.name }}/strong span年龄{{ user.age }}/span /template /UserList /template这里子组件通过:useruser暴露数据父组件通过{ user }接收数据。练习四参考答案一种合理的设计是商品 ID、图片地址、名称、价格通过props传入。促销标签、库存状态如果只是简单状态文本可以使用props如果展示结构差异较大可以使用具名插槽。购买按钮行为可以通过emit通知父组件也可以允许父组件通过插槽完全自定义按钮。商品卡片的基础布局、间距、图片区域由组件自身负责。例如script setup defineProps({ product: { type: Object, required: true } }) const emit defineEmits([buy]) /script template div classproduct-card img :srcproduct.image :altproduct.name / h3{{ product.name }}/h3 p{{ product.price }} 元/p div classproduct-extra slot nameextra/slot /div slot nameactions button clickemit(buy, product) 购买 /button /slot /div /template父组件可以这样使用template ProductCard :productproduct buyhandleBuy template #extra span限时促销/span /template template #actions button clickaddToCart(product) 加入购物车 /button button clickbuyNow(product) 立即购买 /button /template /ProductCard /template这套设计同时使用了props、emit和插槽各自承担不同职责。不过如果父组件已经通过actions插槽完全替换了购买按钮默认的buy事件就不一定会被触发。实际项目中应明确约定使用默认按钮时由组件发出buy事件自定义按钮时由父组件自行处理避免出现职责不清的问题。十二、本篇总结本篇需要真正理解的不是slot的写法而是它背后的组件设计思想。默认插槽父组件向子组件的默认内容区域传入内容。具名插槽父组件向子组件的指定区域传入内容。作用域插槽子组件通过插槽参数暴露数据让父组件决定如何展示。props 适合传递数据和配置插槽适合扩展内容结构。emit 用于通知父组件发生了某个事件。插槽可以帮助通用组件与具体业务解耦但插槽设计也需要适度。最终要建立的思维是组件不一定需要知道内部具体展示什么它可以只负责提供结构、行为和必要的数据接口再把变化的内容交给调用方。这就是插槽在大型 Vue 项目中的核心价值。
延伸阅读

更多相关文章

2026/10/19 4:10:09

Solidity语言的区块链:从0到1编写、测试与部署以太坊智能合约

引言 区块链技术自比特币白皮书发布以来,已经彻底改变了我们对价值转移、数据存储和信任机制的理解。在众多开发区块链的技术栈中,Solidity 是智能合约开发领域当之无愧的霸主。它是一种面向对象的高级编程语言,专为在以太坊虚拟机(EVM)上编写智能合约而设计,受C++、Pyt…

2026/10/19 4:05:08

open-code-review:构建可配置、可量化的代码评审工作流

1. 项目概述1.1 核心需求解析代码评审这件事,几乎所有研发团队都在做,但真正做得好的却不多。我在过去几年的项目里经历过几次团队扩张,从三四个人到十几个人的规模,代码评审的问题一下子就被放大了:合并请求堆积没人看…

2026/10/19 4:05:08

拼音字母量子编程PQLAiQt架构:从叠加态到代码标识映射

1. 先拆概念:这个标题到底在说什么第一次看到“拼音字母量子编程PQLAiQt架构”的人,大概率会愣两秒——这到底是技术名词还是行为艺术?我在项目群里看到这个标题的瞬间,第一反应也是“这不会是某个脑洞大开的梗吧”。但你真正把字…

2026/10/19 5:25:16

C++享元模式实战:从原理到线程安全实现与内存优化

1. 内存告警背后的元凶:上千种弹道特效的重复建模先说个我遇到过的真实场景。某客户端的战斗模块里有一批弹道特效,每个子弹发射后都会在场景里生成一条轨迹,轨迹上挂着一串光点数据。最初功能跑得好好的,但等特效种类从十几个涨到…

2026/10/19 5:25:16

YOLOv8电梯电瓶车检测系统:开箱即用的毕设落地方案

简介:本资源是一套基于YOLOv8的电梯场景电瓶车闯入智能识别与报警系统,面向计算机、人工智能、自动化等专业在校学生及初学者,解决公共场所安全监管中电瓶车违规进电梯这一典型视觉检测问题,适用于毕业设计、课程设计、大作业或项…

2026/10/19 5:25:16

YOLOv8电梯电瓶车实时检测系统:3分钟启动报警GUI

简介:本资源是一套基于YOLOv8的电梯场景电瓶车闯入智能识别与报警系统,面向计算机、人工智能、自动化等专业在校学生及初学者,解决公共场所安全监管中对违规载电动车入梯行为的实时检测与预警需求,特别适合作为毕业设计、课程设计…

2026/10/19 5:25:16

AI工作流优化实战:让智能体不止于单点提效

这两年我帮好几个团队做过AI落地,发现一个特别普遍的现象:大家一提到AI提效,第一反应就是“上一个智能体”。买工具、调接口、套模板,把某个环节换成AI,然后期待奇迹发生。结果呢?局部确实快了,…

2026/10/19 5:20:16

矩阵分解推荐系统实战:从协同过滤到SGD梯度下降原理与实现

如果把推荐系统相关的经典论文排个"被引用最多、被误读也最多"的榜单,这篇《Matrix Factorization Techniques for Recommender Systems》绝对能进前三。我第一次读它的时候刚被各种协同过滤公式折磨完,看完第一反应是:就这么简单&…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/19 0:04:56

悟空开源无代码平台实战:从实体建模到应用设计避坑指南

简介:基于悟空开源无代码平台的多种应用设计源码,面向希望在不编写代码的情况下快速搭建CRM、HRM、财务系统、ERP、SRM等业务应用的企业与技术人员,通过拖放式界面和内置模板即可完成应用定制与部署。压缩包共697个文件,约10.74MB…

2026/10/19 0:04:56

基于.NET9的多租户权限系统:数据权限与动态API落地实践

简介:这份资源是一套面向 .NET 与 Vue 全栈开发者的中台权限管理系统,后端基于 .Net9,前端采用 Vue3 Element Plus,并配套 uniapp uviewui 移动端方案,实现前后端分离。系统覆盖多租户隔离、数据权限、动态 Api、任务…

2026/10/19 0:04:56

场地预约小程序毕设实战:SSM+微信小程序+MySQL数据库设计到避坑

简介:面向毕业设计场景的场地预约小程序完整项目,基于微信小程序SSMMySql技术栈开发,适合计算机相关专业学生作为选题参考、功能扩展或二次开发起点。系统实现场地类型管理、场地管理、预约管理、取消申请与押金退回等功能,分为管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑