插槽 slot:组件版的模板方法模式

为什么需要插槽

组件用 Props 传数据很顺手,但 Props 只能传"值"。想传一段 HTML / 组件 / 复杂结构怎么办?

举个例子:写一个通用的 <Card> 组件。卡片长一样(圆角、阴影、内边距),但内容每次都不一样——首页卡片放笔记摘要、详情页卡片放上一篇下一篇、用户中心卡片放头像和姓名。

如果用 Props:

<!-- ❌ props 传 HTML 字符串,要用 v-html,XSS 风险 -->
<Card content="<h3>标题</h3><p>摘要</p>" />

如果每种内容写一个 Card 变种(NoteCard / UserCard / NavCard)又失去了"Card"的抽象意义。

插槽就是解决这个问题的——Card 负责"壳子",内容留一个"洞",让调用者填。

Java 对照:模板方法模式

public abstract class Card {
    public final void render() {
        renderHeader();
        renderContent();   // 留给子类实现的"洞"
        renderFooter();
    }
    protected abstract void renderContent();
}

class NoteCard extends Card {
    @Override
    protected void renderContent() { /* 渲染笔记 */ }
}

Card.render() 是骨架,renderContent() 是插槽。Vue 的插槽就是这个模式,但用组合代替继承


1. 默认插槽:最简单的"洞"

<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <div class="card-body">
      <slot />  <!-- 留一个洞 -->
    </div>
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0,0,0,.05);
}
</style>

调用方:

<Card>
  <h3>ES6 核心语法</h3>
  <p>从 Java 视角快速过一遍 ES6 的新特性。</p>
</Card>

<Card>
  <img src="/avatar.png" />
  <p>张三</p>
</Card>

内容不同、外壳一致——这就是插槽的价值。

默认内容(fallback)

<!-- Card.vue -->
<slot>暂无内容</slot>

调用时不填内容:<Card /> → 显示 "暂无内容"。填了就显示填的。

Java 对照

protected void renderContent() {
    // 子类没覆盖时的默认实现
    System.out.println("暂无内容");
}

2. 具名插槽:多个"洞"

一个组件常常需要多个位置让外部填。比如布局组件:

<!-- DefaultLayout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header">默认顶栏</slot>
    </header>
    <main>
      <slot />  <!-- 默认插槽,相当于 name="default" -->
    </main>
    <footer>
      <slot name="footer">默认底栏</slot>
    </footer>
  </div>
</template>

调用方用 <template v-slot:xxx>#xxx 简写:

<DefaultLayout>
  <template #header>
    <h1>in4vue</h1>
    <nav>...</nav>
  </template>

  <!-- 默认插槽:不用 template 包裹也行 -->
  <p>这是主体内容</p>

  <template #footer>
    <small>© 2026 yingjf</small>
  </template>
</DefaultLayout>

项目里的例子src/layouts/DefaultLayout.vue 就用了默认插槽(只留一个 <slot /> 给页面填内容)。如果以后首页要自定义顶栏右侧的搜索框,就能加一个 <slot name="header-right" /> 具名插槽。


3. 作用域插槽:子组件把数据"递给"父组件填

这是插槽里最强大的一招。子组件持有数据,但让父组件决定怎么渲染

典型场景:通用列表组件

<!-- DataList.vue -->
<script setup lang="ts">
defineProps<{ items: unknown[] }>()
</script>

<template>
  <ul class="data-list">
    <li v-for="(item, index) in items" :key="index">
      <!-- 把 item 和 index 通过插槽 props 传给父组件 -->
      <slot :item="item" :index="index" />
    </li>
  </ul>
</template>

调用方:

<DataList :items="notes">
  <template #default="{ item, index }">
    <!-- 解构拿到子组件传出来的 item / index -->
    <span>{{ index + 1 }}.</span>
    <strong>{{ item.title }}</strong>
    <span>{{ item.category }}</span>
  </template>
</DataList>

<DataList :items="users">
  <template #default="{ item }">
    <img :src="item.avatar" />
    <span>{{ item.name }}</span>
  </template>
</DataList>

亮点

Java 对照:回调 + 数据

// 策略模式的变体:把"怎么渲染一行"作为函数参数传进来
list.forEach(item -> System.out.println(item.title()));
// 作用域插槽 ≈ list.forEach 里的 lambda

Element Plus 的 <el-table> 大量用作用域插槽:

<el-table :data="notes">
  <el-table-column label="标题">
    <template #default="{ row }">
      <a :href="`/notes/${row.slug}`">{{ row.title }}</a>
    </template>
  </el-table-column>
</el-table>

row 就是子组件(el-table-column)传出来的当前行数据。不会用作用域插槽,就用不好 Element Plus 的表格


4. 三种插槽速查表

类型 子组件写法 父组件写法 用途
默认插槽 <slot /> <Child>...</Child> 一个洞
具名插槽 <slot name="x" /> <template #x>...</template> 多个洞
作用域插槽 <slot :foo="bar" /> <template #default="{ foo }">...</template> 子数据传给父渲染

语法记忆点


5. 新手常见误区

误区 1:想从父组件访问子组件作用域

<Card>
  <!-- ❌ card 里的 internalData 在父作用域访问不到 -->
  <p>{{ internalData }}</p>
</Card>

规则:父组件模板里写的内容,作用域属于父组件。子组件的内部变量想暴露给父,必须通过 slot :xxx="internalData" 作用域插槽的形式。

误区 2:一个作用域插槽传太多东西

<!-- 可以,但不推荐 -->
<slot :a="1" :b="2" :c="3" :d="4" :e="5" />

建议:把多个值打包成一个对象:

<slot :ctx="{ item, index, selected, disabled }" />

<!-- 父组件 -->
<template #default="{ ctx }">
  {{ ctx.item.title }}
</template>

调用方可以只解构需要的字段:{ ctx: { item } }

误区 3:把"应该用组件"的东西塞进插槽

插槽适合内容不固定但骨架固定的场景。如果每次"填"的内容都非常不同,可能本身就该写多个独立组件,而不是硬用一个带插槽的超级组件。

判断标准:看"骨架"是否有复用价值。Card 的骨架(圆角、阴影)复用性高 → 用插槽。要是每个 Card 连边框样式都不一样,那就别勉强。


6. 项目里的落地思考

src/layouts/DefaultLayout.vue 现在只有默认插槽。随着页面变多,可能会演化出:

<template>
  <div class="layout">
    <header>
      <Logo />
      <slot name="header-extra" />   <!-- 各页面自定义顶栏操作区 -->
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <slot name="footer">
        <!-- 默认底栏 -->
      </slot>
    </footer>
  </div>
</template>

这就是模板方法模式在 Vue 里的落地——骨架固定,扩展点标准化。


小练习:给 Card 做个抽屉式组件

  1. 新建 src/components/ContentCard.vue,提供三个插槽:#header, default, #footer
  2. 在首页试着把某张笔记卡片换成:
    <ContentCard>
      <template #header>
        <span class="tag">{{ note.category }}</span>
      </template>
      <h3>{{ note.title }}</h3>
      <p>{{ note.summary }}</p>
      <template #footer>
        <span>{{ note.date }}</span>
      </template>
    </ContentCard>
    
  3. 体会:Card 的边框样式改一次,所有调用方自动跟上

延伸阅读