Code前端首页关于Code前端联系我们

Vue3的slot用法怎么学透?基础、进阶到实战一次讲明白

terry 2小时前 阅读数 9 #SEO
文章标签 Vue3 slot

Vue3里的slot是干什么用的?

简单说,slot(插槽)是Vue里实现分发的工具,想象你做了个“快递盒子”组件,盒子负责样式、布局(比如边框、背景),但盒子里放啥(是放文件、鲜花还是玩具)由用这个组件的父组件决定——slot就是盒子里的“空位”,专门用来装父组件想塞的内容。

举个实际开发场景:做弹窗组件时,不同页面的弹窗里可能要放表单、按钮组、图片等不同内容,如果没有slot,得给弹窗组件写N个props传内容,还得处理HTML结构,特别麻烦,有了slot,弹窗组件里留个<slot>,父组件直接在<弹窗组件>标签里写内容,弹窗负责把内容“嵌”到正确位置,灵活又省心。

怎么用默认插槽?最简单的场景长啥样?

默认插槽是没有名字的插槽,子组件里写个<slot>占位,父组件在子组件标签里直接塞内容就行。

基础用法示例

子组件(MyButton.vue)负责按钮的样式,内容让父组件定:

<template>
  <button class="custom-btn">
    <slot></slot> <!-- 这里是默认插槽,父组件传的内容会替换到这 -->
  </button>
</template>
<style scoped>
.custom-btn { padding: 8px 16px; border: 1px solid #ccc; }
</style>

父组件用的时候,直接在<MyButton>标签里写内容:

<template>
  <MyButton>点击提交</MyButton> <!-- “点击提交”会被放到子组件的slot位置 -->
</template>

给默认插槽加“兜底内容”

如果父组件没传内容,想显示默认文字,子组件的<slot>里可以写默认内容:

<slot>默认按钮</slot> <!-- 父组件没传内容时,显示“默认按钮”;传了就显示父组件的内容 -->

具名插槽在Vue3里怎么写?和Vue2有啥不一样?

当子组件需要多个不同位置的插槽时,就得给slot起名字(具名插槽),比如布局组件要分“头部、主体、底部”三个区域,每个区域用不同名字的slot。

Vue3的具名插槽写法

子组件(Layout.vue)里给slot加name属性:

<template>
  <div class="layout">
    <header><slot name="header"></slot></header>
    <main><slot name="main"></slot></main>
    <footer><slot name="footer"></slot></footer>
  </div>
</template>

父组件用的时候,得用<template>配合v-slot:名字(或缩写#名字)来指定内容该往哪个slot里塞:

<Layout>
  <template #header> <!-- #header 是 v-slot:header 的缩写 -->
    <h1>页面大标题</h1>
  </template>
  <template #main>
    <p>这里是正文内容...</p>
  </template>
  <template #footer>
    <p>©2024 版权信息</p>
  </template>
</Layout>

和Vue2的区别

Vue2里是直接在标签上加slot="名字"

<!-- Vue2 写法,现在Vue3不推荐了 -->
<header slot="header">标题</header>

Vue3把具名插槽的语法统一到<template>v-slot上,避免了“普通标签加slot属性可能混淆作用域”的问题,写法更规范,也更易维护。

作用域插槽怎么理解?实际开发哪里能用到?

作用域插槽是子组件给插槽传数据,父组件用的时候能拿到这些数据,比如子组件是个“Todo列表”,每个列表项的结构父组件想自定义,但数据(比如每个Todo的文字、是否完成)在子组件里——这时候作用域插槽就能让子组件把数据“传给”父组件的插槽内容。

代码示例:Todo列表自定义渲染

子组件(TodoList.vue)里,循环渲染Todo时,把每个todo数据传给slot:

<template>
  <ul class="todo-list">
    <li v-for="todo in todos" :key="todo.id">
      <slot :todo="todo"></slot> <!-- 把todo对象传给插槽,父组件能拿到 -->
    </li>
  </ul>
</template>
<script setup>
import { ref } from 'vue'
const todos = ref([
  { id: 1, text: '吃饭', done: false },
  { id: 2, text: '睡觉', done: true }
])
</script>

父组件用的时候,通过v-slot="接收的数据"拿到子组件传的todo:

<TodoList>
  <template #default="slotProps"> <!-- #default 对应默认插槽,也可以省略不写 -->
    <input type="checkbox" :checked="slotProps.todo.done" /> 
    {{ slotProps.todo.text }}
  </template>
</TodoList>

觉得slotProps太长?还能解构赋值简化:

<template #default="{ todo }"> <!-- 直接把slotProps里的todo解构出来 -->
  <input type="checkbox" :checked="todo.done" /> 
  {{ todo.text }}
</template>

实际开发场景

  • 表格组件:列的内容(比如操作按钮、带tooltip的文字)由父组件定,但行数据在子组件里,用作用域插槽传每行数据;
  • 下拉选择器:选项的显示样式(比如带图标、带标签)由父组件定,选项数据在子组件里,用作用域插槽传选项数据;
  • 卡片组件:卡片底部的操作按钮由父组件定,但卡片数据(比如标题、描述)在子组件里,用作用域插槽传卡片数据。

插槽和props、emit有啥不同?能互相替代吗?

props、emit、slot是Vue组件通信的三种方式,场景完全不同,没法互相替代

方式 作用 传递的是… 典型场景
props 父组件→子组件传数据 纯数据(字符串、对象等) 给子组件传标题、配置项等
emit 子组件→父组件触发事件 事件(带可选参数) 子组件点击按钮,通知父组件处理
slot 父组件→子组件传“结构/内容” HTML结构、组件组合 给弹窗传表单、给列表传自定义项

举个“弹窗组件”的例子更清楚: 用props传:<Popup title="提示" />

  • 弹窗关闭时用emit通知父组件:<button @click="$emit('close')">关闭</button>
  • 弹窗里的表单内容用slot传:<Popup><Form /></Popup>

所以三者是互补关系,根据需求选合适的方式就行。

在Composition API组件里用slot要注意什么?

Vue3的Composition API(比如<script setup>)写组件时,用slot和选项式API差别不大,但有个实用技巧:useSlots()判断插槽是否存在

子组件里判断插槽是否存在

比如做个布局组件,想根据“是否有header插槽”来决定是否渲染头部区域:

<template>
  <div class="layout-comp">
    <div class="header" v-if="hasHeader">
      <slot name="header"></slot>
    </div>
    <div class="content">
      <slot></slot>
    </div>
  </div>
</template>
<script setup>
import { useSlots } from 'vue' // 导入useSlots
const slots = useSlots() // 获取所有插槽
const hasHeader = !!slots.header // 判断header插槽是否存在
</script>

这样父组件没传header插槽时,子组件就不渲染头部区域,避免空标签占位影响样式。

插槽嵌套、动态插槽名这些进阶玩法怎么实现?

实际开发中,插槽不止用一层,还能玩“动态切换插槽名”,这俩技巧能让组件灵活性拉满。

插槽嵌套:父组件传的内容里再放插槽

比如做个“按钮包装器”组件(ButtonWrapper),负责按钮的样式,而按钮里的图标由更上层的父组件(祖父组件)决定:

  • 子组件(ButtonWrapper.vue):

    <template>
    <div class="btn-wrapper">
      <slot></slot> <!-- 接收父组件传的按钮 -->
    </div>
    </template>
  • 父组件(用ButtonWrapper时,给按钮里加slot):

    <ButtonWrapper>
    <MyButton>
      <slot name="icon"></slot> <!-- 这个slot由祖父组件传 -->
      点击提交
    </MyButton>
    </ButtonWrapper>
  • 祖父组件(给icon插槽传内容):

    <ButtonWrapper>
    <template #icon>
      <Icon name="plus"></Icon> <!-- 自定义图标 -->
    </template>
    </ButtonWrapper>

动态插槽名:插槽名由变量控制

比如做主题切换组件,不同主题下要渲染不同名字的插槽,Vue3里用v-slot:[变量名](或缩写#[变量名])实现:

父组件里,插槽名由变量slotName控制:

<template>
  <ThemeComponent>
    <template v-slot:[slotName]> <!-- 动态绑定插槽名 -->
      这是{{ slotName }}插槽的内容
    </template>
  </ThemeComponent>
</template>
<script setup>
import { ref } from 'vue'
const slotName = ref('dark-content') // 变量控制插槽名
</script>

子组件(ThemeComponent.vue)里对应多个插槽:

<template>
  <div class="theme">
    <slot name="dark-content"></slot>
    <slot name="light-content"></slot>
  </div>
</template>

slotName变成light-content时,就会渲染light-content对应的插槽内容,适合动态切换布局、主题的场景。

开发UI组件库时,slot有哪些常见设计思路?

做UI组件库时,插槽是让组件“可扩展”的核心武器,常见设计思路有这几种:

布局型组件:多插槽拆分区域

比如Layout组件分headerasidemainfooter,用户能自由组合页面结构:

<template>
  <div class="layout">
    <header><slot name="header"></slot></header>
    <aside><slot name="aside"></slot></aside>
    <main><slot name="main"></slot></main>
    <footer><slot name="footer"></slot></footer>
  </div>
</template>

用户用的时候,想怎么布局就怎么传内容,组件库只负责提供“区域容器”。

功能型组件:留扩展插槽

比如Table组件,列头、单元格、页脚都留插槽,让用户自定义:

<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.key">
          <slot name="header" :col="col">{{ col.title }}</slot> <!-- 列头默认显示标题,用户可自定义 -->
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <td v-for="col in columns" :key="col.key">
          <slot name="cell" :row="row" :col="col">{{ row[col.key] }}</slot> <!-- 单元格默认显示数据,用户可自定义 -->
        </td>
      </tr>
    </tbody>
    <tfoot>
      <slot name="footer"></slot> <!-- 页脚让用户自定义分页、汇总等 -->
    </tfoot>
  </table>
</template>

用户可以给列头加筛选按钮,给单元格加编辑图标,自由度拉满。

状态型组件:内容插槽+兜底

比如Empty组件(空状态),默认显示“暂无数据”,用户也能传自定义内容:

<template>
  <div class="empty">
    <slot>
      <img src="@/assets/empty.png" />
      <p>暂无数据</p>
    </slot>
  </div>
</template>

时显示默认空状态,传了就显示用户内容,兼顾通用性和扩展性。

用slot容易踩的坑有哪些?怎么避坑?

插槽看似简单,实际开发稍不注意就会踩坑,这几个常见问题要警惕:

不渲染:检查slot和传值位置

  • 子组件没写<slot>:再好的内容没地方放也白搭,记得子组件里留<slot>占位;
  • 具名插槽没写对name:父组件<template #name>name要和子组件<slot name="name">name完全一致,拼写错一个字母都不行;
  • 默认插槽传内容时,父组件直接写标签里,不用包<template>(除非和具名插槽混用)。

作用域插槽拿不到数据:检查数据传递和解构

  • 子组件没给slot传数据:比如要传todo="todo",漏写v-bind就传不过去;
  • 父组件解构名字错了:子组件传的是item="item",父组件用{ list }接收,名字对不上就拿不到。

Vue2和Vue3语法混淆:牢记Vue3规则

Vue3里具名插槽必须用<template #name>,不能像Vue2那样在普通标签上加slot="name",新项目直接用Vue3语法,旧项目迁移时全局替换。

插槽嵌套导致作用域丢失:中间层要透传数据

比如祖父组件给父组件传slot带数据,父组件给子组件传的时候,得把接收到的slotProps再通过v-bind传给子组件的slot:

父组件(中间层)接收并透传数据:

<ChildComponent>
  <template #default="slotProps">
    <GrandChildComponent>
      <slot :data="slotProps.data"></slot> <!-- 把数据透传给子组件的slot -->
    </GrandChildComponent>
  </template>
</ChildComponent>

Vue3插槽和React的Children有什么区别?

虽然Vue的slot和React的Children都解决“组件内容自定义”问题,但实现思路不一样:

  • Vue的slot是“显式占位”:子组件用<slot>明确告诉父组件“我这里要插内容”,父组件用v-slot明确告诉子组件“我要插在这个位置”,还能通过作用域插槽传数据,语法糖多,写起来直观;
  • React的Children是“隐式传递”:子组件通过this.props.children获取父组件传的内容,要实现“子传父数据”(类似Vue作用域插槽),得用render props模式(子组件传个函数,父组件调用函数传数据)。

举个“自定义列表项”的例子对比:

Vue(作用域插槽):

<Child>
  <template #default="{ item }">{{ item.name }}</template>
</Child>

React(render props):

<Child render={(item) => <div>{item.name}</div>} />

Vue的slot更像“预先留好位置等内容”,React的Children更像“把内容当参数传递”,核心都是让组件更灵活,但Vue的语法对新手更友好,React的写法更偏函数式编程。

写在最后:
slot是Vue组件化开发里“让组件既复用又灵活”的关键,从最基础的默认插槽,到能传数据的作用域插槽,再到组件库级别的插槽设计,掌握这些用法后,写组件时再也不用纠结“内容怎么让用户自定义”,多在项目里练手(比如封装个带插槽的表格、弹窗),自然能吃透slot的精髓~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门