十、插槽与组件封装

一、插槽

如果说 props 是让父组件给子组件传“数据”,那么插槽就是让父组件给子组件传“HTML 内容”。

为了让你秒懂,我们用一个生活中的例子来理解:
插槽就像是一台“游戏机”(子组件),游戏机本身只负责提供外壳和运行逻辑,但里面留了一个“卡槽”(插槽)。你可以往里面插不同的“游戏卡带”(父组件传入的 HTML 内容),从而让这台游戏机展现出不同的游戏画面。

在 Vue 3 中,插槽主要分为三大类。

1. 默认插槽(Default Slot)

场景:子组件只需要一个“坑位”来接收父组件的内容。

子组件 (MyCard.vue):用 <slot></slot> 占位。你可以把它想象成一个“默认内容”,如果父组件没传东西,就显示默认内容。

<template>
  <div class="card">
    <!-- 插槽出口:父组件传的内容会渲染在这里 -->
    <slot>
      <p>这是默认内容,如果父组件不传,就显示我</p>
    </slot>
  </div>
</template>

父组件:直接在子组件标签内部写 HTML 即可。

<template>
  <MyCard>
    <h2>我是父组件传进来的标题</h2>
    <p>我是父组件传进来的正文</p>
  </MyCard>
</template>

2. 具名插槽(Named Slots)

场景:子组件有多个“坑位”。比如一个后台布局组件,需要分别接收“头部”、“侧边栏”和“主内容区”。

子组件 (BaseLayout.vue):通过 name 属性给插槽命名。

<template>
  <div class="layout">
    <header>
      <slot name="header"></slot> <!-- 头部坑位 -->
    </header>
    <main>
      <slot></slot> <!-- 没写 name 的,就是默认坑位 -->
    </main>
    <footer>
      <slot name="footer"></slot> <!-- 底部坑位 -->
    </footer>
  </div>
</template>

父组件:使用 <template> 标签配合 v-slot(简写为 #)来精准投放内容。

<template>
  <BaseLayout>
    <!-- 投放到 header 插槽 -->
    <template #header>
      <h1>后台管理系统</h1>
    </template>

    <!-- 投放到默认插槽 -->
    <p>这里是页面的主要内容区域</p>

    <!-- 投放到 footer 插槽 -->
    <template #footer>
      <span>Copyright © 2026</span>
    </template>
  </BaseLayout>
</template>

3. 作用域插槽(Scoped Slots)—— 进阶核心

场景:子组件不仅提供“坑位”,还把子组件内部的数据传回给父组件,让父组件根据这些数据来决定“坑位里长什么样”。

最经典的实际开发场景:封装一个通用的列表组件。

子组件 (UserList.vue):在 <slot> 上绑定属性,把内部数据暴露出去。

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <!-- 把当前遍历的 user 对象作为插槽 prop 传出去 -->
      <slot name="userItem" :user="user">
        {{ user.name }} <!-- 默认显示名字 -->
      </slot>
    </li>
  </ul>
</template>

父组件:通过 v-slot=“slotProps” 接收子组件传过来的数据,自由定制 UI。

<template>
  <UserList :users="userList">
    <!-- 接收子组件传来的数据,这里用解构语法更简洁 -->
    <template #userItem="{ user }">
      <!-- 父组件完全控制了怎么渲染这个 user -->
      <span style="color: red;">VIP用户:{{ user.name }}</span>
    </template>
  </UserList>
</template>

4. 注意事项

  • 渲染作用域隔离:插槽内容是在父组件的模板里写的,所以它只能访问父组件的数据,不能直接访问子组件的数据(除非通过作用域插槽传出来)。
  • 默认内容会被覆盖:只要父组件提供了插槽内容(哪怕里面是空的 <template #header></template>),子组件 <slot> 标签里的默认内容就绝对不会显示。
  • Vue 3 语法统一:在 Vue 2 时代,具名插槽用 slot=“header”,作用域用 slot-scope。在 Vue 3 中,这些旧语法已被废弃,请全部统一使用 v-slot 及其简写 #。
  • 独占默认插槽的缩写:如果组件只有默认插槽,你可以把 v-slot 直接写在组件标签上:
    如果同时有默认插槽和具名插槽,默认插槽必须用 <template #default> 写,不能直接写在组件标签上。
<!-- 只有默认插槽时,可以这样简写 -->
<MyCard v-slot="{ data }">
  <p>{{ data }}</p>
</MyCard>

二、页面卡片组件封装

实际场景:在后台管理系统中,我们通常不会把表格或表单直接裸露在页面上,而是用一个带有圆角、阴影的"卡片"把它们包裹起来。而且,卡片的标题旁边往往还需要放一些操作按钮(比如"新增"、“导出”)。

1. 封装子组件 (ContentWrap.vue)

子组件 ContentWrap.vue 负责提供卡片的整体骨架,通过 具名插槽默认插槽 分别预留标题操作区和内容区,让父组件灵活定制。

<template>
  <div class="content-wrap">
    <!-- 头部区域:支持传入标题,并在右侧留出插槽 -->
    <div class="wrap-header" v-if="title">
      <span class="title">{{ title }}</span>
      <!-- 具名插槽:父组件可以往这里塞按钮 -->
      <div class="header-actions">
        <slot name="header"></slot>
      </div>
    </div>
    <!-- 默认插槽:包裹核心内容 -->
    <div class="wrap-body">
      <slot></slot>
    </div>
  </div>
</template>

<script setup>
defineProps({
  title: String
})
</script>

2. 父组件使用

父组件通过 #header 具名插槽向标题右侧投放操作按钮,默认插槽则直接放入表格内容,实现卡片与业务逻辑的解耦。

<template>
  <!-- 使用封装好的卡片,并传入标题 -->
  <ContentWrap title="员工列表">
    <!-- 通过具名插槽,自定义标题右侧的按钮 -->
    <template #header>
      <button @click="handleAdd">新增员工</button>
      <button @click="handleExport">导出</button>
    </template>

    <!-- 默认插槽里放表格内容 -->
    <table>
      <tr><td>张三</td><td>前端</td></tr>
    </table>
  </ContentWrap>
</template>

三、数据表格组件封装

实际场景:企业里到处是表格。每个表格上面通常都有一个"工具栏"(左侧是标题,右侧是搜索框或刷新按钮)。如果每个页面都手写工具栏,代码会极其冗余。

1. 封装子组件 (ProTable.vue)

ProTable.vue 将工具栏和表格主体封装在一起,并通过 作用域插槽 把当前行数据 row 暴露给父组件,让父组件自由定制操作列的内容。

<template>
  <div class="pro-table">
    <!-- 工具栏区域 -->
    <div class="toolbar">
      <div class="left"><slot name="toolbar-left"></slot></div>
      <div class="right"><slot name="toolbar-right"></slot></div>
    </div>

    <!-- 表格主体 -->
    <table>
      <thead><tr><th>姓名</th><th>年龄</th><th>操作</th></tr></thead>
      <tbody>
        <tr v-for="row in tableData" :key="row.id">
          <td>{{ row.name }}</td>
          <td>{{ row.age }}</td>
          <td>
            <!-- 作用域插槽:把当前行数据 row 传递给父组件 -->
            <slot name="action" :row="row"></slot>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
defineProps({
  tableData: Array
})
</script>

关键点<slot name="action" :row="row"> 这一行是作用域插槽的典型应用——子组件在遍历表格数据时,把每一行的 row 对象作为插槽 prop 抛出,父组件通过解构语法 { row } 拿到当前行数据后,即可自由渲染"编辑"、"删除"等操作按钮。

2. 父组件使用

父组件使用 ProTable 时,通过 #toolbar-left#toolbar-right 这两个具名插槽分别定制工具栏左右两侧的内容,再通过 #action="{ row }" 作用域插槽获取当前行数据,实现行级操作。

<template>
  <ProTable :tableData="userList">
    <!-- 自定义工具栏左侧 -->
    <template #toolbar-left>
      <h3>用户管理</h3>
    </template>

    <!-- 自定义工具栏右侧 -->
    <template #toolbar-right>
      <input placeholder="搜索用户..." />
      <button>刷新</button>
    </template>

    <!-- 自定义操作列:接收子组件传过来的 row 数据 -->
    <template #action="{ row }">
      <button @click="edit(row)">编辑</button>
      <button @click="del(row.id)">删除</button>
    </template>
  </ProTable>
</template>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值