插槽(Slot)实战:默认插槽、具名插槽、作用域插槽、插槽的复用场景

插槽这个东西,说白了就是 Vue 组件里的一块「预留区域」。你写组件的时候,有些内容不确定,那就留个坑,让使用者来填。我刚开始学 Vue 时,觉得插槽不就是个占位符吗?后来做复杂组件才发现,它其实是组件复用性的灵魂。

今天咱们就把插槽的四种形态——默认插槽、具名插槽、作用域插槽、以及复用场景,一次性讲透。

一、默认插槽:最简单的占位方式

先看最基础的。你写一个卡片组件,卡片标题和内容不确定,那就用 <slot> 留个位置。

<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">卡片标题</div>
    <div class="card-body">
      <slot></slot>
    </div>
  </div>
</template>

使用时,直接往组件标签里塞内容:

<Card>
  <p>这是一段卡片内容</p>
</Card>

如果没传内容,<slot> 里可以写默认值:

<slot>默认显示的内容</slot>
我的习惯:每个插槽都写默认内容。哪怕只是一个空注释,也能让后来者知道这里「可以插东西」。

二、具名插槽:一个组件多个坑

一个组件只有一个插槽?那太局限了。比如一个布局组件,有头部、侧边栏、主内容区,每个区域都需要独立插槽。

这时候就用 name 属性:

<!-- Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header">默认头部</slot>
    </header>
    <aside>
      <slot name="sidebar">默认侧边栏</slot>
    </aside>
    <main>
      <slot name="main">默认主内容</slot>
    </main>
  </div>
</template>

使用时,用 v-slot 指定名字:

<Layout>
  <template v-slot:header>
    <h1>网站标题</h1>
  </template>
  <template v-slot:sidebar>
    <ul><li>导航1</li></ul>
  </template>
  <template v-slot:main>
    <p>正文内容</p>
  </template>
</Layout>

v-slot 可以简写为 #

<template #header>...</template>
我曾经踩过的坑:在 Vue 2 里,具名插槽和默认插槽混用时,默认插槽必须用 v-slot:default 包裹,否则会报错。Vue 3 修复了这个问题,但如果你还在维护老项目,千万注意。

三、作用域插槽:让父组件访问子组件的数据

默认插槽和具名插槽,都是父组件往子组件里塞「静态内容」。但有时候,子组件内部有数据,想让父组件决定「怎么渲染」——这就是作用域插槽的用武之地。

举个例子,一个列表组件:

<!-- List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="index">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: '苹果', price: 5 },
        { id: 2, name: '香蕉', price: 3 }
      ]
    }
  }
}
</script>

父组件使用时,通过 v-slot 接收数据:

<List>
  <template v-slot:default="slotProps">
    <span>{{ slotProps.item.name }} - ¥{{ slotProps.item.price }}</span>
  </template>
</List>

也可以解构:

<List>
  <template v-slot="{ item }">
    <span>{{ item.name }} - ¥{{ item.price }}</span>
  </template>
</List>
核心理解:作用域插槽的本质,是「子组件提供数据,父组件决定视图」。这给了父组件极大的灵活性,尤其适合表格、列表、下拉框这类需要自定义渲染的组件。

四、插槽的复用场景:实战中的三个典型

光讲理论没意思。我挑三个真实项目里高频出现的场景,咱们看看插槽怎么用。

场景一:弹窗组件

弹窗的标题、内容、底部按钮,每个项目都不一样。用插槽最合适:

<!-- Modal.vue -->
<template>
  <div class="modal-overlay" v-if="visible">
    <div class="modal">
      <div class="modal-header">
        <slot name="title">默认标题</slot>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <slot name="footer">
          <button @click="close">关闭</button>
        </slot>
      </div>
    </div>
  </div>
</template>

使用时,想怎么定制都行:

<Modal :visible="showModal">
  <template #title>确认删除?</template>
  <p>删除后无法恢复,确定要删除吗?</p>
  <template #footer>
    <button @click="cancel">取消</button>
    <button @click="confirm" class="danger">确认删除</button>
  </template>
</Modal>

场景二:表格列自定义

表格组件里,每一列的渲染方式可能不同。有的列要显示图片,有的要显示状态标签,有的要显示操作按钮。作用域插槽完美解决:

<!-- Table.vue -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.key">{{ col.title }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <td v-for="col in columns" :key="col.key">
          <slot :name="col.key" :row="row" :value="row[col.key]">
            {{ row[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

使用时,对特定列做定制:

<Table :columns="columns" :data="list">
  <template #status="{ value }">
    <span :class="value === 'active' ? 'green' : 'gray'">
      {{ value === 'active' ? '启用' : '停用' }}
    </span>
  </template>
  <template #action="{ row }">
    <button @click="edit(row)">编辑</button>
    <button @click="delete(row)">删除</button>
  </template>
</Table>

场景三:表单组件

表单的每个表单项,验证规则、布局、样式都可能不同。用插槽把「表单项内容」和「表单容器」解耦:

<!-- FormItem.vue -->
<template>
  <div class="form-item">
    <label>{{ label }}</label>
    <div class="form-control">
      <slot :error="error"></slot>
      <span v-if="error" class="error-msg">{{ error }}</span>
    </div>
  </div>
</template>

使用时,传入任意类型的输入控件:

<FormItem label="用户名" :error="errors.username">
  <template v-slot="{ error }">
    <input 
      v-model="form.username" 
      :class="{ 'is-error': error }"
      placeholder="请输入用户名"
    />
  </template>
</FormItem>
复用技巧:我习惯把插槽的「数据接口」提前定义好。比如作用域插槽暴露哪些属性,写在组件文档里。这样团队其他人用起来,一看就知道能拿到什么数据。

五、总结一下

插槽这东西,用好了能让组件复用性翻倍。我个人的经验是:

  • 默认插槽:适合简单的内容嵌入,比如卡片、弹窗主体
  • 具名插槽:适合多区域布局,比如弹窗的标题、内容、底部
  • 作用域插槽:适合需要父组件自定义渲染的场景,比如表格列、列表项
  • 复用场景:弹窗、表格、表单、列表、布局组件,都是插槽的高频使用地

你想想看,如果没有插槽,每个组件都得写死内容,那还谈什么复用?插槽就是 Vue 组件化里「开放封闭原则」的最佳实践——对扩展开放,对修改封闭。

下一章咱们聊聊「混入(Mixin)与组合式函数(Composables)」,看看怎么把组件里的逻辑抽出来复用。到时候你会发现,插槽负责视图复用,混入和组合式函数负责逻辑复用,两者配合才是完整的组件化方案。