第 1 节 概述
前一篇文章讲了 props。props 传递的是数据 。
但如果父组件想传的不是数据,而是一段 HTML 模板呢?
比如你有一个"卡片"组件,它的边框、标题栏都是固定的,但中间的内容每次不同。用 props 传 HTML 字符串会很难处理。
Vue 3 提供了插槽(Slot) 来解决这个问题。它让父组件可以向子组件内部的指定位置"塞入"模板内容。
第 2 节 默认插槽
2.1 基本用法
子组件中用 <slot> 占位,父组件在标签之间填写内容:
1
2
3
4
5
6
<!-- FancyButton . vue -->
< template >
< button class = "fancy-btn" >
< slot />
</ button >
</ template >
1
2
3
4
5
6
7
<!-- Parent . vue -->
< template >
< FancyButton > 点击我 </ FancyButton >
< FancyButton >
< span style = "color: red" > 删除 </ span >
</ FancyButton >
</ template >
渲染结果:
1
2
< button class = "fancy-btn" > 点击我</ button >
< button class = "fancy-btn" >< span style = "color: red" > 删除</ span ></ button >
<slot> 就是子组件中开的"口子",父组件的内容会填到那个位置。
2.2 后备内容
如果父组件没传内容,可以给 slot 设置默认显示:
1
2
3
4
5
6
<!-- SubmitButton . vue -->
< template >
< button type = "submit" >
< slot > 提交 </ slot >
</ button >
</ template >
1
2
3
4
5
6
7
< template >
<!-- 不传内容时显示 "提交" -->
< SubmitButton />
<!-- 传内容时覆盖 -->
< SubmitButton > 保存 </ SubmitButton >
</ template >
2.3 渲染作用域
插槽的内容是在父组件中定义的,所以它只能访问父组件的数据,不能访问子组件的数据。
1
2
3
4
5
6
< template >
< FancyButton >
{{ parentMessage }} <!-- ✅ 父组件的数据 -->
{{ childMessage }} <!-- ❌ 子组件的数据 , 拿不到 -->
</ FancyButton >
</ template >
第 3 节 具名插槽
一个组件可能需要在多个位置插入内容。比如布局组件,需要 header、main、footer 三个插槽。
这时可以用 name 属性区分:
1
2
3
4
5
6
7
8
9
10
11
12
<!-- BaseLayout . vue -->
< template >
< header >
< slot name = "header" />
</ header >
< main >
< slot /> <!-- 不写 name 就是默认插槽 , name 为 "default" -->
</ main >
< footer >
< slot name = "footer" />
</ footer >
</ template >
父组件用 v-slot 指令指定内容对应哪个插槽:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
< template >
< BaseLayout >
< template # header >
< h1 > 网站标题 </ h1 >
</ template >
< template # default >
< p > 主要内容 </ p >
</ template >
< template # footer >
< p > 版权信息 </ p >
</ template >
</ BaseLayout >
</ template >
# 是 v-slot: 的简写。#header 等价于 v-slot:header。
技巧
非 template 的内容会自动归为默认插槽。上面的 #default 可以省略,直接把内容写在组件标签之间即可。
第 4 节 作用域插槽
默认情况下,插槽内容只能访问父组件的数据。
但有时候,子组件需要把它的数据"吐"给插槽内容使用。比如一个列表组件,它负责循环和加载状态,但每一项怎么渲染由父组件决定。
子组件向 slot 传递数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- FancyList . vue -->
< template >
< ul >
< li v-for = "item in items" :key="item.id" >
< slot name = "item" :data = "item" />
</ li >
</ ul >
</ template >
< script setup lang = "ts" >
const items = ref ([
{ id : 1 , title : '文章一' , author : '张三' },
{ id : 2 , title : '文章二' , author : '李四' },
])
</ script >
父组件用 #name="props" 接收数据:
1
2
3
4
5
6
7
8
9
10
< template >
< FancyList >
< template # item = "{ data }" >
< div >
< strong >{{ data . title }}</ strong >
< span > —— {{ data . author }}</ span >
</ div >
</ template >
</ FancyList >
</ template >
这样,数据的控制和展示就分开了。子组件管数据(从哪里加载、怎么分页),父组件管展示(每项长什么样)。
4.1 默认插槽的作用域
默认插槽也可以用作用域:
1
2
3
4
<!-- Child . vue -->
< template >
< slot text = "hello" :count = "1" />
</ template >
1
2
3
4
5
< template >
< Child v-slot = "{ text, count }" >
< p >{{ text }} {{ count }}</ p >
</ Child >
</ template >
注意
当有具名插槽时,默认插槽需要用 template 包裹才能接收作用域数据。
第 5 节 条件插槽
有时候,子组件的外层结构依赖于是否传入了插槽内容。
比如一个卡片组件,如果没传 header,就不渲染 header 区域。
用 $slots 可以判断:
1
2
3
4
5
6
7
8
9
10
11
12
13
< template >
< div class = "card" >
< div v-if = "$slots.header" class="card-header" >
< slot name = "header" />
</ div >
< div class = "card-content" >
< slot />
</ div >
< div v-if = "$slots.footer" class="card-footer" >
< slot name = "footer" />
</ div >
</ div >
</ template >
这样,没传入 header 时,header 的 DOM 元素根本不会出现,页面结构更干净。
第 6 节 插槽 vs props
对比 props 插槽 传递什么 数据(字符串、数字、对象等) 模板片段 谁渲染 子组件渲染 父组件渲染后嵌入子组件 灵活性 子组件决定如何展示 父组件决定展示方式 适用场景 配置项、数据传递 布局、自定义内容
插槽让组件的组合方式更灵活。父组件可以在子组件内部自由布置内容。
但到目前为止,我们只讲了相邻组件(父子)之间的通信。
如果组件层级很深呢?爷爷传给孙子,要经过几层 props 传递。
这个问题的解决方案是 provide / inject ,也是下一篇要讲的内容之一。