孟凡的博客blog.mengfanyaoapp.com
课程资料库›Vue 3 + Element Plus 前端开发›第8次 · 插槽 slot 与动态组件
模块总览

第8次 · 插槽 slot 与动态组件

2026-09-14 · Vue 3 + Element Plus 前端开发 · 课程笔记
课堂练习资料 08-插槽slot与动态组件.zip
下载 ZIP 压缩包

第8次 · 插槽 slot 与动态组件

第1/7页 · 先知道今天要做什么

上一课你把卡片拆成了子组件。但马上就遇到新问题:子组件的“内容”是写死的。比如你做了一个 <Panel title="成绩">,里面固定显示成绩列表;现在另一处想用同一个盒子装“课程安排”,只能再复制一个组件出来改。

插槽(slot)就是解决这个问题的:把组件做成一个“盒子”,盒子的外观由组件负责,盒子里面装什么由使用者决定。 这是从“会写组件”到“会设计组件”的关键一步,也是 Element Plus 里 el-card、el-dialog、el-table 这些组件能千变万化的原因。

今天还要顺带解决另一个问题:同一个位置想显示三种不同的视图(卡片 / 表格 / 原始数据),难道写三个 v-if?——用动态组件 <component :is> 更合适。

今天的场景

一个数据面板。你写两个通用组件:Panel(带标题和右上角操作区的盒子)和 ListBox(负责循环、并把每条数据“暴露”给使用者自己渲染)。然后用这两个组件拼出三种视图,用一个单选按钮切换视图。

下课时拿出这三样结果

做到什么 在哪里看 完成的证据
会用默认插槽与具名插槽 Panel 组件 标题栏右上角显示自定义内容,盒子内部显示你写的内容。
会用作用域插槽 ListBox 组件 循环的每一项由父组件决定长什么样,且能拿到该项的数据和下标。
会用动态组件与 keep-alive 视图切换区 切换视图时界面整体替换;切回来时组件内部的状态被保留。

你只需要带着这些基础来

会 props、emits、v-for、v-if;第 7 课的工程还能跑。

本课时 45 分钟怎么上

时间 老师带着做 学生留下什么
0—5 分钟 提问:el-card 的 #header 是怎么把标题塞进去的? 意识到“标签中间的内容”也是被组件接收的。
5—20 分钟 讲默认插槽、具名插槽、作用域插槽,三者各解决什么问题。 笔记里写下三者的使用场景与写法。
20—32 分钟 讲动态组件 <component :is> 与 keep-alive,说明“切换被销毁”的问题。 能说出 keep-alive 解决什么。
32—42 分钟 粘贴本课 App.vue,切换三种视图,观察状态是否保留。 三种视图都能切换。
42—45 分钟 抽查:问“作用域插槽里那个 { item } 是谁给的”。 全员提交。

从哪里开始

复制第 7 课的工程,改名 lesson-vue-08。不需要新增依赖。

今天先掌握到这里

今天讲的是组件的“内容定制能力”。第 9 课讲生命周期与模板引用,第 10 课做阶段综合练习。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(插槽 / 动态组件章节)· 组件依据 Element Plus 2.8 官方文档


第2/7页 · 把新词说清楚

新词与本课含义

名字 用熟悉的话理解 本课里长什么样
插槽 slot 组件里预留的“空位”,使用者往里填内容。 <slot />
默认插槽 没有名字的那个空位,标签中间的内容会填进去。 <Panel>这里的内容</Panel>
具名插槽 有名字的空位,用 <template #名字> 指定填到哪个位置。 <template #extra>...</template>
作用域插槽 空位由子组件负责传数据,父组件负责渲染。 <template #default="{ item }">
动态组件 用一个特殊标签,根据数据决定“现在显示哪个组件”。 <component :is="currentComp" />
keep-alive 把切走的组件“留着”,不要销毁,回来时状态还在。 <keep-alive><component :is="c" /></keep-alive>

三种插槽的对比

谁决定位置 谁决定内容 典型用途
默认插槽 子组件 父组件 盒子里装什么
具名插槽 子组件(多个位置各有名字) 父组件 头部标题、右上角按钮、页脚
作用域插槽 子组件(决定循环与位置) 父组件(决定长什么样) 表格列、列表项、树节点

写法对照

<!-- 子组件(真实项目中往往是单独文件) -->
<template>
  <div class="panel">
    <div class="panel-hd">
      <slot name="title">默认标题</slot>      <!-- 具名插槽,带默认内容 -->
      <slot name="extra" />                   <!-- 具名插槽 -->
    </div>
    <div class="panel-bd">
      <slot />                                <!-- 默认插槽 -->
    </div>
  </div>
</template>
<!-- 父组件使用 -->
<Panel>
  <template #title>学员成绩</template>
  <template #extra>
    <el-tag size="small">共 5 人</el-tag>
  </template>
  <p>这里的内容会进默认插槽</p>
</Panel>

三个要点:

  1. 插槽可以写默认内容(<slot>默认标题</slot>),使用者不填就显示默认值;
  2. 具名插槽用 #名字,等价于 v-slot:名字;
  3. 使用具名插槽时,<template> 的顺序不影响显示位置——位置由子组件里的 <slot> 决定,这一点和你想的正好相反,一定要记住。

作用域插槽:为什么需要它

普通的默认插槽有一个致命限制:父组件往插槽里放内容时,拿不到子组件的数据。

比如 ListBox 负责循环 items,可是每一项长什么样,父组件想自己决定(有的地方显示姓名,有的地方显示价格)。父组件拿不到 item,就写不了。于是子组件在提供空位的同时,把数据一起递出来:

<!-- 子组件里 -->
<div v-for="(item, index) in items" :key="index">
  <slot :item="item" :index="index" />   <!-- 把数据递给插槽 -->
</div>
<!-- 父组件里:用解构接住 -->
<ListBox :items="list">
  <template #default="{ item, index }">
    <span>{{ index + 1 }}. {{ item.name }}({{ item.score }} 分)</span>
  </template>
</ListBox>

一句话区分:普通插槽是“我给你个位置”;作用域插槽是“我给你个位置,顺手把数据也给你”。

动态组件与 keep-alive

<script setup>
import { ref } from 'vue'
const current = ref('card')
</script>

<template>
  <el-radio-group v-model="current">
    <el-radio-button value="card">卡片</el-radio-button>
    <el-radio-button value="table">表格</el-radio-button>
  </el-radio-group>

  <component :is="views[current]" />
  <!-- 等价写法:<component :is="current" /> 需要组件被全局注册 -->
</template>

<component :is> 的取值可以是:注册过的组件名字符串,或组件对象本身。本课用第二种(更明确,不容易出错)。

不加 keep-alive 会发生什么? 每次切换,旧组件被销毁、新组件被重新创建。组件内部的状态(例如你在视图里输入的字、滚动位置)会丢失。加上 keep-alive 就会缓存起来。

写法 切走再回来 什么时候用
<component :is="c" /> 重新创建,状态丢失 视图很重、不需要保留状态
<keep-alive><component :is="c" /></keep-alive> 从缓存恢复,状态保留 标签页式切换、表单填写中切换

只看这几行:插槽的最小骨架

<!-- 子组件:预留一个位置,并把数据递出去 -->
<script setup>
defineProps({ items: { type: Array, default: () => [] } })
</script>
<template>
  <div v-for="(item, i) in items" :key="i">
    <slot :item="item" :index="i">默认显示 {{ item }}</slot>
  </div>
</template>

先口头预测:上面这个子组件如果父组件什么插槽都不填,页面上会显示什么?

显示“默认显示 X”。因为 <slot> 标签中间的那段内容就是兜底内容,只有在使用者没提供插槽内容时才出现。这和上一课 props 的 default 是同一个思路:给一个合理的默认值。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(插槽 / 动态组件章节)· 组件依据 Element Plus 2.8 官方文档


第3/7页 · 先看一个小例子

本课使用的 App.vue(单文件,含两个内联子组件)

和上一课一样,为了满足“每课只有一个 App.vue、粘贴即用”,两个子组件用 h() 渲染函数定义在同一个文件里。插槽在 h() 里表现为 slots.xxx(),与模板里的 <slot name="xxx"> 一一对应,讲义第 7 页会有对照表。

<script setup>
import { ref, computed, defineComponent, h } from 'vue'
import {
  ElCard, ElRadioGroup, ElRadioButton, ElTag, ElTable, ElTableColumn,
  ElEmpty, ElAlert, ElButton, ElMessage
} from 'element-plus'
import 'element-plus/dist/index.css'

/* ================= 子组件 1:Panel(具名插槽 + 默认插槽) ================= */
const Panel = defineComponent({
  name: 'Panel',
  props: { title: { type: String, default: '未命名面板' } },
  setup(props, { slots }) {
    return () => h('div', { class: 'panel' }, [
      h('div', { class: 'panel-hd' }, [
        h('span', { class: 'panel-title' }, props.title),
        // 具名插槽 extra:使用者不填就是空
        h('span', { class: 'panel-extra' }, slots.extra ? slots.extra() : null)
      ]),
      h('div', { class: 'panel-bd' },
        // 默认插槽:没有默认内容,不填就是空白
        slots.default ? slots.default() : null)
    ])
  }
})

/* ================= 子组件 2:ListBox(作用域插槽) ================= */
const ListBox = defineComponent({
  name: 'ListBox',
  props: { items: { type: Array, default: () => [] } },
  setup(props, { slots }) {
    return () => {
      if (!props.items.length) {
        return h('div', { class: 'list-empty' }, '暂无数据')
      }
      return h('div', { class: 'list-box' },
        props.items.map((item, index) => {
          // 把 item 与 index 递给父组件的插槽
          const children = slots.default ? slots.default({ item, index }) : String(item)
          return h('div', { class: 'list-item', key: item.id ?? index }, children)
        })
      )
    }
  }
})

/* ================= 三个视图组件(用于动态组件演示) ================= */
// 视图 1:卡片列表
const CardView = defineComponent({
  props: { items: Array },
  setup(props) {
    return () => h('div', { class: 'grid' },
      props.items.map(it => h(ElCard, { class: 'mini', shadow: 'never', key: it.id }, {
        default: () => [
          h('b', null, it.name),
          h('p', { class: 'muted' }, `${it.cls} · ${it.score ?? '未提交'}`)
        ]
      })))
  }
})

// 视图 2:表格
const TableView = defineComponent({
  props: { items: Array },
  setup(props) {
    return () => h(ElTable, { data: props.items, size: 'small' }, {
      default: () => [
        h(ElTableColumn, { prop: 'name', label: '姓名', width: 100 }),
        h(ElTableColumn, { prop: 'cls', label: '班级', width: 130 }),
        h(ElTableColumn, { prop: 'score', label: '得分', width: 90 }),
        h(ElTableColumn, { label: '状态' }, {
          default: ({ row }) => h(ElTag, { size: 'small', type: row.done ? 'success' : 'info' },
            () => (row.done ? '已提交' : '未提交'))
        })
      ]
    })
  }
})

// 视图 3:带内部状态的组件,用来演示 keep-alive 是否保留状态
const CounterView = defineComponent({
  setup() {
    const n = ref(0)
    return () => h('div', { class: 'counter' }, [
      h('p', null, `这是没有 keep-alive 的普通视图。我在这个组件里点了几次:${n.value}`),
      h(ElButton, { type: 'primary', onClick: () => { n.value++ } }, () => '点我 +1')
    ])
  }
})

const CachedCounterView = defineComponent({
  setup() {
    const n = ref(0)
    return () => h('div', { class: 'counter' }, [
      h('p', null, `这个视图被 keep-alive 缓存了。我点了几次:${n.value}`),
      h(ElButton, { type: 'success', onClick: () => { n.value++ } }, () => '点我 +1')
    ])
  }
})

/* ================= 父组件 ================= */
const list = ref([
  { id: 1, name: '张三', cls: '计算机1班', done: true, score: 92 },
  { id: 2, name: '李四', cls: '计算机1班', done: false, score: null },
  { id: 3, name: '王五', cls: '计算机2班', done: true, score: 78 },
  { id: 4, name: '赵六', cls: '计算机2班', done: true, score: 45 }
])

const current = ref('CardView')
const views = { CardView, TableView, CounterView, CachedCounterView }

const stat = computed(() => {
  const done = list.value.filter(s => s.done).length
  return { total: list.value.length, done }
})
</script>

<template>
  <div class="page">
    <h1 class="title">插槽与动态组件</h1>

    <!-- ① 具名插槽 #title / #extra,默认插槽装内容 -->
    <Panel title="学员成绩">
      <template #extra>
        <el-tag size="small" type="success">已提交 {{ stat.done }} / {{ stat.total }}</el-tag>
        <el-button size="small" text type="primary" @click="list.push({ id: Date.now(), name: '新同学', cls: '计算机3班', done: false, score: null })">
          加一个人
        </el-button>
      </template>

      <!-- ② 作用域插槽:每一项长什么样,由这里决定 -->
      <ListBox :items="list">
        <template #default="{ item, index }">
          <div class="row">
            <span class="idx">{{ index + 1 }}</span>
            <b class="nm">{{ item.name }}</b>
            <span class="muted">{{ item.cls }}</span>
            <el-tag size="small" :type="item.done ? 'success' : 'info'">
              {{ item.done ? (item.score ?? '已提交') : '未提交' }}
            </el-tag>
          </div>
        </template>
      </ListBox>
    </Panel>

    <!-- ③ 同样的 ListBox,换一种渲染方式:只显示名字,用逗号隔开 -->
    <Panel title="只有名字的一种渲染(同一个 ListBox)">
      <ListBox :items="list">
        <template #default="{ item }">
          <span class="chip">{{ item.name }}</span>
        </template>
      </ListBox>
    </Panel>

    <!-- ④ 动态组件 + keep-alive -->
    <Panel title="动态组件演示">
      <template #extra>
        <el-radio-group v-model="current" size="small">
          <el-radio-button value="CardView">卡片视图</el-radio-button>
          <el-radio-button value="TableView">表格视图</el-radio-button>
          <el-radio-button value="CounterView">未缓存</el-radio-button>
          <el-radio-button value="CachedCounterView">已缓存</el-radio-button>
        </el-radio-group>
      </template>

      <!-- 没有 keep-alive:切走再回来,内部计数会归零 -->
      <component v-if="current === 'CounterView'" :is="views.CounterView" />

      <!-- 有 keep-alive:切走再回来,内部计数还在 -->
      <keep-alive>
        <component v-if="current === 'CachedCounterView'" :is="views.CachedCounterView" />
      </keep-alive>

      <component v-if="current === 'CardView'" :is="views.CardView" :items="list" />
      <component v-if="current === 'TableView'" :is="views.TableView" :items="list" />
    </Panel>

    <el-alert
      title="实验:切到“未缓存”点几次 +1,切到别的视图再切回来,数字归零;换成“已缓存”重复一次,数字保留。"
      type="info" :closable="false" show-icon class="tip"
    />
  </div>
</template>

<style>
html, body, #app {
  margin: 0; padding: 0; max-width: none; width: auto; height: auto;
  display: block; place-items: initial; text-align: left; background: #f5f7fa;
}
</style>

<style scoped>
.page { max-width: 1000px; margin: 28px auto; padding: 0 16px; }
.title { font-size: 22px; color: #303133; margin: 0 0 18px; }
.panel { border: 1px solid #e4e7ed; border-radius: 8px; background: #fff; margin-bottom: 16px; overflow: hidden; }
.panel-hd {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 16px; border-bottom: 1px solid #ebeef5; background: #fafafa;
}
.panel-title { font-weight: 600; color: #303133; }
.panel-extra { display: flex; gap: 8px; align-items: center; }
.panel-bd { padding: 14px 16px; }
.list-item { border-bottom: 1px dashed #ebeef5; padding: 8px 0; }
.list-item:last-child { border-bottom: none; }
.row { display: flex; align-items: center; gap: 12px; }
.idx { width: 20px; color: #a8abb2; }
.nm { color: #303133; }
.muted { color: #909399; font-size: 13px; }
.chip {
  display: inline-block; background: #ecf5ff; color: #409eff;
  padding: 2px 10px; border-radius: 12px; font-size: 12px;
}
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.mini { border: 1px solid #ebeef5; }
.list-empty { color: #909399; padding: 12px 0; }
.counter p { margin: 0 0 10px; color: #303133; }
.tip { margin-top: 6px; }
</style>

预期看见什么

  1. 第一个面板:标题“学员成绩”,右上角有一个绿色统计标签和一个“加一个人”按钮,主体是带序号、姓名、班级、状态标签的行列表。
  2. 第二个面板:标题“只有名字的一种渲染”,主体是同一批数据,但显示成了一排蓝色圆角小标签。注意:两个面板用的是同一个 ListBox 组件。
  3. 第三个面板:右上角四个单选按钮,切换后主体区域整体换成卡片、表格、计数器。
  4. 切到“未缓存”点 3 次 +1,切到“已缓存”再切回来——“未缓存”的计数归零,“已缓存”的计数保留。
  5. 点“加一个人”,两个面板的数据同时多一行(因为它们共用同一个 list)。

先猜一猜,读完代码再回答

第二个面板里,ListBox 收到的插槽内容是:

<template #default="{ item }">
  <span class="chip">{{ item.name }}</span>
</template>

第一个面板里则是:

<template #default="{ item, index }">
  <div class="row">...四列内容...</div>
</template>

同一个组件,为什么能渲染出完全不同的东西?

因为 ListBox 只负责两件事:循环数据、给每一项留一个位置。至于“每一项长什么样”,它完全不关心,交给使用者在插槽里写。 而 item、index 这些数据是 ListBox 在调用 slots.default({ item, index }) 时“递”出来的——这就是作用域插槽。它把“循环的控制权”和“渲染的控制权”分给了两个不同的组件,这正是通用组件的设计方式。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(插槽 / 动态组件章节)· 组件依据 Element Plus 2.8 官方文档


第4/7页 · 一步一步做出来

第1步:先看默认插槽

<!-- 子组件 Box.vue(真实项目中的独立文件) -->
<template>
  <div class="box"><slot /></div>
</template>
<!-- 父组件 -->
<Box>我塞进来的内容</Box>

预期:Box 的边框里显示“我塞进来的内容”。把内容去掉,盒子变成空壳。

第2步:给插槽一个默认值

<!-- 子组件 -->
<div class="box"><slot>我是默认内容</slot></div>

父组件不填时,预期:显示“我是默认内容”。

第3步:加一个具名插槽

<!-- 子组件 Box.vue -->
<template>
  <div class="box">
    <div class="hd"><slot name="title">无标题</slot></div>
    <div class="bd"><slot /></div>
  </div>
</template>
<!-- 父组件:注意两个 template 的写法 -->
<Box>
  <template #title>我的标题</template>
  <p>正文</p>
</Box>

预期:标题出现在头部,正文出现在下面。

再做一次反直觉实验:把父组件里两个 template 的顺序调换,把 <p>正文</p> 写在 #title 前面。预期:显示位置完全不变。因为位置是由子组件里的 <slot> 决定的,父组件只负责“填哪一格”。

第4步:用作用域插槽做循环

<!-- 子组件 ListBox.vue -->
<script setup>
defineProps({ items: { type: Array, default: () => [] } })
</script>
<template>
  <div v-for="(item, index) in items" :key="item.id ?? index">
    <slot :item="item" :index="index" />
  </div>
</template>
<!-- 父组件 -->
<ListBox :items="list">
  <template #default="{ item, index }">
    {{ index + 1 }} - {{ item.name }}
  </template>
</ListBox>

预期:数据按行渲染,序号和姓名都在。

第5步:体验动态组件被销毁

<script setup>
import { ref } from 'vue'
import A from './A.vue'
const current = ref('A')
</script>
<template>
  <button @click="current = current === 'A' ? 'B' : 'A'">切换</button>
  <component :is="current === 'A' ? A : B" />
</template>

在 A 里放一个计数器,点几次 +1,切到 B 再切回来。预期:数字归零。

第6步:加上 keep-alive 再试

<keep-alive>
  <component :is="current === 'A' ? A : B" />
</keep-alive>

预期:切回来数字还在。

第7步:整段替换为本课完整版

把第 3 页的完整 App.vue 覆盖 src/App.vue,把四个视图和两个插槽实验都跑一遍。

本课文件速查

位置 本次作用
Panel 的 slots.extra() 具名插槽 #extra 的渲染位置。
Panel 的 slots.default() 默认插槽的渲染位置。
ListBox 的 slots.default({ item, index }) 把数据递给父组件,实现作用域插槽。
<component :is="views[current]" /> 动态组件:按变量切换显示哪个组件。
<keep-alive> 缓存被切走的组件状态。

完成修改后检查

操作或输入 预期观察
不填 #extra 面板右上角为空白(没有报错)。
交换两个 template 顺序 显示位置不变。
清空 list ListBox 显示“暂无数据”。
在同一页用两次 ListBox 两种渲染方式都生效。
切走再切回“已缓存”视图 计数保留。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(插槽 / 动态组件章节)· 组件依据 Element Plus 2.8 官方文档


第5/7页 · 换成自己的代码

你的任务

  1. 写一个自己的盒子组件:Panel 的具名插槽再加一个 #footer,并在父组件里给它填上“最后更新于 xx”的说明文字。
  2. 用作用域插槽做一个完全不同的渲染:同一个 ListBox,第三处使用改成显示“头像圆圈 + 姓名首字”(例如 <div class="avatar">{{ item.name[0] }}</div>)。
  3. 加一个视图:给动态组件再加一个视图 EmptyView,显示一句“这块还没做”,并加入单选按钮。
  4. 验证 keep-alive 的边界:把两个计数器视图都放进同一个 keep-alive 里(用 :is 切换),观察两个组件的状态是否各自被保留。写下结论。
  5. 插槽默认值实验:给 ListBox 的插槽加兜底内容(在 h() 里写成 slots.default ? slots.default({item,index}) : String(item.id)),然后写一处不填插槽的 ListBox 用法,观察兜底内容出现。

按这张操作单完成

步骤 改哪里 运行后应该看到什么
1 加页脚插槽 Panel + 使用处 面板底部出现说明文字。
2 第三种渲染 第三个 ListBox 使用处 显示头像圆圈与首字。
3 加视图 新增 EmptyView + 单选按钮 切到该视图显示提示文字。
4 缓存边界 keep-alive 包裹 :is 两个计数器各自保留自己的数字。
5 插槽兜底 ListBox + 不填插槽的用法 显示兜底内容。

最低完成标准:先把这三项做实

检查项 通过条件
三种插槽 默认插槽、具名插槽、作用域插槽各至少用到一次且能说清区别。
动态组件 至少 3 个视图能切换,且 :is 绑的是组件对象。
能解释 keep-alive 能用自己的话说明“切走时发生了什么、加了 keep-alive 后有什么不同”。

在本地写练习记录

  1. 写下三种插槽各自的“谁决定位置、谁决定内容”。
  2. 记录 keep-alive 实验的两组数据(未缓存 / 已缓存 切走再回来分别是多少)。
  3. 抄下你写的第三个 ListBox 使用处代码。

最后交什么

提交 src/App.vue 与 练习记录.md。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(插槽 / 动态组件章节)· 组件依据 Element Plus 2.8 官方文档


第6/7页 · 出错时从哪里查

把现象和检查动作对上

看到什么现象 / 报错 先检查哪里 修好后怎样确认
插槽内容不显示 用了 #default 但子组件里没有 <slot>;或子组件忘了渲染插槽。 子组件里补上 <slot /> 后正常。
具名插槽填错了位置 名字不一致(子组件 name="title",父组件写成 #titles)。 名字统一后正常。
点“切换视图”后输入的内容没了 没加 keep-alive。 加上后保留。
<component :is="'card'"> 不显示 传的是字符串但组件没有全局注册。 改成传组件对象,例如 views[current]。
控制台提示组件被做成响应式 组件对象放进了 reactive / ref。 把它放在普通变量里,或用 markRaw。
作用域插槽里 item 是 undefined 子组件调用 slots.default() 时忘了把数据传出去。 改成 slots.default({ item, index })。
用 h() 写插槽时报 slots.default is not a function 使用者没填这个插槽,而代码里直接调用。 加判断 slots.default ? slots.default() : null。

排错口诀

插槽位置子组件定,插槽内容父组件填; 具名插槽名字要对,写错一个字母就不显示; 作用域插槽靠“递”,子组件不递父组件拿不到; 切走状态丢,是没加 keep-alive。

不要用这些办法“修复”

  1. 不要为了“让插槽有内容”在子组件里写一份默认 UI,同时父组件又填一份——两份内容会同时出现。要默认值就用 <slot>默认值</slot> 的写法。
  2. 不要用一堆 props(例如 headerText、footerText、bodyText)来替代插槽。插槽能传结构,props 只能传值,一旦需要传按钮、图标、复杂布局,props 就撑不住了。
  3. 不要给所有动态组件都套 keep-alive。缓存会占内存,只在确实需要保留状态的标签页式切换里用。

需要老师帮忙时交这些

子组件里插槽的声明代码、父组件里使用处的代码、Console 报错原文、你期望在哪里看到什么、实际出现在哪里。

老师巡视时只问三句话

  1. 这个位置是默认插槽还是具名插槽?怎么判断的?(能指出子组件里的 <slot name> / slots.xxx)
  2. 作用域插槽里的 item 是谁给的?(能答出子组件递出来的)
  3. 你的视图切换有没有保留状态?为什么?(能答出 keep-alive)

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(插槽 / 动态组件章节)· 组件依据 Element Plus 2.8 官方文档


第7/7页 · 用自己的话讲一遍

回到第 3 页:你刚才猜对了吗?

为什么同一个 ListBox 能渲染出两种完全不同的东西?

参考解释:ListBox 只承担“循环”和“留位置”的职责,每一项的内容完全由使用者在插槽中决定。数据则由 ListBox 通过插槽作用域递出去。这就是“逻辑在组件里、样式在调用处”的通用组件设计方式——Element Plus 的 el-table、el-select 都是这个套路。

模板写法与 h() 写法对照

模板写法 h() 渲染函数写法
<slot /> slots.default()
<slot name="extra" /> slots.extra()
<slot :item="item" :index="i" /> slots.default({ item, index })
<template #default="{ item }">...</template> 由父组件在 h() 里用 { default: ({item}) => [...] } 提供
<template #extra>...</template> { extra: () => [...] }

回答本课的两个问题

问题 参考解释
普通插槽和作用域插槽的区别是什么? 普通插槽只提供位置;作用域插槽在这个位置上还把子组件的数据递出来,让父组件能基于数据渲染。
keep-alive 解决什么问题? 组件切换时默认会被销毁重建,内部状态随之丢失。keep-alive 把切走的组件缓存下来,切回来直接复用,状态得以保留。

五个必须能答上来的问题

考考自己 可以这样通俗回答
具名插槽的简写是什么? #名字,完整写法是 v-slot:名字。
模板里两个 <template> 的书写顺序会影响显示位置吗? 不会,位置由子组件里的 <slot> 决定。
插槽可以写默认内容吗? 可以,写在 <slot> 标签中间,使用者不填时才显示。
<component :is="x"> 里的 x 可以是什么? 已注册组件的名字字符串,或组件对象本身(推荐后者)。
什么时候不该用 keep-alive? 视图很重、数量很多,或者根本不需要保留状态时。

一分钟现场演示指南

  1. 展示通用性:同一个 ListBox 两种渲染方式并排,说明“组件只给骨架,皮肤由调用方决定”。
  2. 展示插槽位置:现场交换两个 template 顺序,指出显示顺序不变。
  3. 展示 keep-alive:两个计数器对比,切走再回来,一个归零一个保留。
  4. 收尾一句:“插槽让组件能被定制,keep-alive 让切换不丢状态。”

交作业前最后检查

  • src/App.vue 与 练习记录.md 已保存。
  • 三种插槽各有至少一处使用。
  • 动态组件至少 3 个视图可切换。
  • keep-alive 实验现象记录完整。
  • Console 无红色报错。

课后再做一个小对照

打开 Element Plus 官方文档的 el-card 组件页,找出它有哪些插槽(提示:header、footer、默认插槽),然后用 el-card 的插槽重写本课第一个面板的头部与底部,看看能不能少写一个自定义 Panel。写下一句话:什么时候该用现成组件的插槽,什么时候值得自己写一个盒子组件。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(插槽 / 动态组件章节)· 组件依据 Element Plus 2.8 官方文档