Vue slot 5大应用场景

235 阅读6分钟

大家好!今天给大家介绍的是Vue中slot 的5大应用场景。在Vue 开发中,组件显示的内容我们通常都是通过props 进行组件传值的。但是有些时候我们更喜欢像写平常的标签一样直接写在标签之间。甚至有些时候我们封装的组件有一部分的模板内容和样式可能需要自定义,props 就可能不是那么的方便了。这时候我们就需要使用Vue 提供的另一个功能slot了。

使用场景一、按钮组件封装

UiButton.vue 组件

<template>
  <button class="ui-button">
    <slot>点击</slot>
  </button>
</template>
<script setup>
</script>
<style lang="scss">
.ui-button {
  background: #fff;
  border:1px solid #dcdfe6;
  border-radius: 4px;
  padding: 8px 15px;
  outline: none;

  &:focus{
    border: 1px solid #646cff;
  }
}
</style>

使用UIButton 按钮组件

<template>
  <div>
    <UiButton></UiButton>
    <UiButton class="btn">按钮</UiButton>
    <UiButton @click="submit" class="btn">
      <img src="@/assets/images/loading.gif" width="14" v-if="isLoading" />
      提交
    </UiButton>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import UiButton from '@/components/UiButton/index.vue'

const isLoading = ref(false)
const submit = () => {
  isLoading.value = true

  setTimeout(() => { //模拟提交表单过程
    isLoading.value = false
  }, 2000)
}
</script>

<style lang="scss" scoped>
.btn {
  margin-left: 20px;
}
</style>


在上面案例中:

  • 我们封装了UiButton组件,组件中使用了slot。
  • 在使用UiButton 按钮时,第一个按钮我们没有在组件中间写任何东西,他就会显示组件UiButton slot中默认的文案,
  • 第二个按钮我们写了按钮两个字,UiButton 中的slot 内容就会被替换为按钮两个字。
  • 第三个按钮,我们在UiButton中间插入了一张loading图片和提交两个字,这时候,UiButton slot中的内容就会被替换为图片和提交,当然初始化的时候loading图片不会显示,因为我们加了if判断。点击之后就会显示,这是一个模拟表单提交的过程。

看看运行效果

点击提交按钮之前:

image.png

点击提交之后:

image.png

使用场景二、弹框组件封装

UiDialog.vue

<template>
  <div class="ui-wrap">
    <div class="ui-dialog">
      <h3 class="header">
      {{ title }}
      <a href="javascript:void(0)" @click="close">X</a>
      </h3>
      <div class="body">
        <slot>弹框内容</slot>
      </div>
      <div class="footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</template>
<script setup>

const emit = defineEmits(['close'])
const close = () => {
  emit('close')
}
const props = defineProps({
  title: "弹框标题"
})
</script>
<style lang="scss" scoped>
a {
  text-decoration: none;
  color: #333;
}
.ui-wrap{
  position: fixed;
  left: 0;
  top: 0;
  background: rgba(0, 0, 0, 0.5);
  width: 100%;
  height: 100%;

  .ui-dialog {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border-radius: 4px;
    width: 620px;
    height: 320px;
    background: #fff;

    .header{
      height: 50px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 20px;
      border-bottom: 1px solid #dcdfe6;
      padding: 0 30px;
      margin: 0;
    }

    .body {
      padding: 20px 30px;
      height: 180px;
    }
    
    .footer {
      display: flex;
      justify-content: flex-end;
      padding: 0 30px;
    }
  }
}
</style>

使用UiDialog.vue组件

<template>
  <div>
    <UiButton @click="open">显示弹框</UiButton>
    <UiDialog title="删除确认框" v-if="isShow" @close="close">
      <p>确认删除一条数据吗?</p>
      <template v-slot:footer>
        <UiButton @click="close">取消</UiButton>
        <UiButton class="btn primary" @click="confirm">确认</UiButton>
      </template>
    </UiDialog>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import UiButton from '@/components/UiButton/index.vue'
import UiDialog from '@/components/UiDialog/index.vue'

const isShow = ref(false)

const close = () => {
  isShow.value = false
}

const open = () => {
  isShow.value = true
}

const confirm = () => {
  isShow.value = false

  // 其他操作
}
</script>

<style lang="scss" scoped>
.btn {
  margin-left: 20px;
}
.primary {
  background: #409eff;
  color: #fff;
}
</style>

在上面的案例中:

  • 我们使用了默认插槽和具名插槽。
  • 弹框内容,我们使用默认插槽,子组件中没有写名称的slot 就是默认插槽,在使用时,不需要加template, 当然加上template 也是可以使用的,需要在template 上加上 v-slot:default 或者简写的 #default, 比如下面的代码
<template v-slot:default>
  <p>确认删除一条数据吗?</p>
</template>
  • 底部我们使用具名插槽。子组件上使用name="footer" 定义名称, 使用时使用v-slot:footer 指定渲染到哪个插槽名称里面
  • 弹框标题我们直接使用props, 因为标题一般来说都是纯文本,使用slot 的必要性不是太大

运行效果:

image.png

使用场景三、卡片组件封装

UiCard.vue 组件封装

<template>
  <div class="ui-card">
    <div class="card-header" v-if="$slots.cardHeader">
      <slot name="cardHeader"></slot>
    </div>
    <div class="card-body" v-if="$slots.cardBody">
      <slot name="cardBody"></slot>
    </div>
    <div class="card-footer" v-if="$slots.cardFooter">
      <slot name="cardFooter"></slot>
    </div>
  </div>
</template>
<script setup>


</script>
<style lang="scss" scoped>
.ui-card {
  box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);
  border: 1px solid #e4e7ed;
  border-radius: 4px;

  .card-header {
    border-bottom: 1px solid #e4e7ed;
    line-height: 22px;
    padding: 18px 20px;
    font-size: 18px;
  }

  .card-body {
    padding: 20px;
  }

  .card-footer {
    border-bottom: 1px solid #e4e7ed;
    padding: 18px 20px;
  }
}
</style>
<template>
  <div class="wrap">
    <UiCard class="card">
      <template v-slot:cardHeader>
        最新文章
      </template>
      <template v-slot:cardBody>
        <ul>
          <li v-for="item in list">
            <a href="#">{{ item.title }}</a>
          </li>
        </ul>
      </template>
      <template v-slot:cardFooter>
        <p class="footer-p">
          <a href="https://www.szft.gov.cn/bmxx_qt/qjwjw/ttxw/index.html">更多</a>
        </p>
      </template>
    </UiCard>

    <UiCard class="card">
      <template v-slot:cardHeader>
        羊肉串10快一窜
      </template>
      <template v-slot:cardBody>
        <div class="img-wrap">
          <img src="https://b0.bdstatic.com/c38091fed675e84d50bc118402670801.jpg@h_1280" />
        </div>
      </template>
    </UiCard>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import UiCard from '@/components/UiCard/index.vue'

const list = ref([
  { id: 1, title: '中央军委主席习近平签署命令 发布新修订的《军队装备科研条例》'},
  { id: 2, title: '中央层面整治形式主义为基层减负专项工作机制会议在京召开'},
  { id: 3, title: '习近平春节前夕视察慰问部队'},
  { id: 4, title: '习近平同摩纳哥元首阿尔贝二世亲王就中摩建交30周年互致贺电'},
  { id: 5, title: '中国共产党第二十届中央纪律检查委员会第四次全体会议公报'}
])
</script>

<style lang="scss" scoped>
.wrap {
  display: flex;
}
.card {
  max-width: 500px;
  margin-left: 20px;
  overflow: hidden;

  ul {
    padding: 0 0 0 20px;
    margin: 0;

    li {
      margin: 10px 0;
      a {
        text-decoration: none;
        color: #333;
      }
    }
  }
  .img-wrap {
    height: 260px;
    overflow: hidden;

    img {
      max-width: 100%;
    }
  }

  .footer-p {
    margin: 0;
  }
}
</style>

在这个案例中:

  • 我们使用具名插槽分别定义了卡片的头部, 卡片的内容, 卡片的底部
  • 我们使用了作用域插槽, 在子组件中我们使用$slots.插槽名称 判断使用者是否传入了插槽对应的内容,如果传入了,就渲染,没传入就不渲染。这样做的好处是避免不需要渲染的插槽占用空间和多余样式
  • 使用时第一个卡片我们传入了子组件所需要的所有内容,第二个组件则只传入了卡片底部和卡片内容

看看运行效果:

image.png

使用场景四、封装table组件

UiTable 组件

<template>
  <table class="ui-table">
    <thead>
      <tr>
        <slot name="tHeaderTd"></slot>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in tableData" :key="index">
        <slot name="tBodyTd" :row="item"></slot>
      </tr>
    </tbody>
  </table>
</template>
<script setup>
const props = defineProps({
  tableData: {
    type: Array,
    default () {
      return []
    }
  }
})

</script>
<style lang="scss" scoped>
.ui-table{
  border-collapse: collapse; /* 合并边框 */
  border: 1px solid #dcdfe6; /* 设置表格外边框 */
  width: 100%;
  tr {
    border-bottom: 1px solid #dcdfe6;
    :deep(td){
      padding: 10px;
    }
  }
}
</style>

使用UiTable 组件;

<template>
  <div class="wrap">
    <UiTable :tableData="list">
      <template v-slot:tHeaderTd>
        <td>日期</td>
        <td>姓名</td>
        <td>地址</td>
      </template>
      <template v-slot:tBodyTd="{ row }">
        <td>{{ row.date }}</td>
        <td>{{ row.name }}</td>
        <td>{{ row.address }}</td>
      </template>
    </UiTable>
  </div>
</template>
<script setup>
import UiTable from '@/components/UiTable/index.vue'

const list = [
  {
    date: '2016-05-03',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-04',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-01',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
]
</script>

<style lang="scss" scoped>

</style>

在上面的案例中:

  • 我们使用了具名插槽分别定义了表格头部的内容和表格正文内容该放在哪个内容里
  • 使用通过props 将tableData 传入组件
  • 使用作用域插槽的特性将值传到模板中,子组件中通过 :row="item"使用时 通过v-slot:tBodyTd="{ row }"进行接收。 这样我们在模板中就可以通过row 取到每个字段了。

来看看效果:

image.png

使用场景五、后台管理系统框架搭建

UiLayout.vue 组件封装

<template>
  <div class="ui-layout">
    <header>
      <slot name="layHeader"></slot>
    </header>
    <aside>
      <slot name="laySideMenu"></slot>
    </aside>
    <main>
      <slot name="layContent"></slot>
    </main>
    <footer>
      <slot name="layFooter"></slot>
    </footer>
  </div>
</template>
<script setup>

</script>
<style lang="scss" scoped>
header {
  display: flex;
  height: 60px;
  position: fixed;
  left: 0;
  top: 0;
  background: #E6E8EB;
  align-items: center;
  width: 100%;
  justify-content: space-between;
  padding: 0 20px;
  box-sizing: border-box;
  z-index: 2;
}
aside {
  width: 220px;
  height: 100%;
  position: fixed;
  left: 0;
  top: 0;
  padding: 90px 20px;
  background: #303133;
}
main {
  margin-left: 250px;
  margin-top: 60px;
}
footer {
  height: 60px;
  background: #FAFAFA;
}
</style>

使用UiLayout组件

<template>
  <div class="wrap">
    <UiLayout>
      <template v-slot:layHeader>
        <div class="log">
          Logo
        </div>
        <nav>
          <a href="javascript:void(0)">淘宝</a>
          <a href="javascript:void(0)">天猫</a>
          <a href="javascript:void(0)">京东</a>
          <a href="javascript:void(0)">拼多多</a>
        </nav>
        <div class="info">
          小明
        </div>
      </template>

      <template v-slot:laySideMenu>
        <ul class="aside-menu">
          <li><a href="javascript:void(0)">仪表盘</a></li>
          <li><a href="javascript:void(0)">商品管理</a></li>
          <li><a href="javascript:void(0)">用户管理</a></li>
          <li><a href="javascript:void(0)">个人中心</a></li>
        </ul>
      </template>
      <template v-slot:layContent>
        <div class="content">
          <p>欢迎来到后台管理系统</p>
        </div>
      </template>
      <template v-slot:layFooter>
        <div class="footer">
          <p>这里时底部信息,底部信息!!</p>
        </div>
      </template>
    </UiLayout>
  </div>
</template>
<script setup>
import UiLayout from '@/components/UiLayout/index.vue'
</script>

<style lang="scss" scoped>
a {
  text-decoration: none;
}

nav {
  display: flex;
  a {
    padding: 0 20px;
  }
}

.aside-menu {
  list-style: none;
  a {
    display: block;
    padding: 10px 0;
    color: #fff;
  }
}

.content {
  display: flex;
  align-items: center;
  justify-content: center;
  height: calc(100vh - 120px);
  p{
    font-size: 40px;
  }
}

.footer {
  text-align: center;
  line-height: 60px;
  p {
    margin: 0;
  }
}
</style>

在上面的案例中我们主要使用了具名插槽,定义了 头部,底部, 侧边栏,内容面板,搭建了一个初略的后台管理框架。用户可以自定义相关的内容。

总结:

Vue中的slot机制提供了一种灵活的方式来定制和扩展组件的内容。在组件封装过程中,合理使用slot可以提高组件的复用性和灵活性,使得组件更容易适应不同的业务需求。通过本文介绍的五大应用场景,相信开发者可以更好地理解和应用Vue中的slot机制,提升开发效率和代码质量。