切换语言为:繁体

封装 Tab 组件,在组件中使用插槽时遇到的问题

  • 爱糖宝
  • 2024-07-05
  • 2113
  • 0
  • 0

封装 Tab 组件,在组件中使用插槽时遇到的问题

最近由于公司项目使用 vue3vant 重构,并且所有的页面都需要按照设计图来,在使用 vant 组件库的 tabs 组件时,我发现组件的样式和设计图完全不一样,并且如果要修改样式,只能使用 :deep() 穿透来直接修改组件的样式,考虑到 tabs 组件我这边使用起来很简单,只需要切换即可,所以决定自己写一个简单的,以下是 tabs 组件的代码:

tabs.vue

<template>
  <div>
    <render-tab-bar/>
    <render-content/>
  </div>
</template>
<script setup>
import {ref, useSlots, h} from 'vue'
const globalProps = defineProps({
  name: String,
  default: Number
})
const slots = useSlots()
let currentTab = ref(globalProps.default || 0)
const emit = defineEmits(['update:default'])

const dealClick = (tab) => {
  emit('update:default', tab)
  currentTab.value = tab
}

const renderOneButton = (name, tab, index) =>
    h(
    'label',
    {
      class: {
        'tab-bar-button-item': true,
        'tab-bar-button-item-active': currentTab.value === tab
      }
    },
    [
      h(
          'input',
          {
            style: {
              display: 'none'
            },
            type: 'radio',
            name: globalProps.name,
            value: name,
            onclick: () => dealClick(tab)
          },
          {}
      ),
      name
    ]
    )

const renderTabBar = () =>
    h(
        'div',
    {
      class: "tab-bar-button-list"
    },
        slots.default &&
        slots.default().map((item, index) => {
          return renderOneButton(item.props?.name, item.props?.tab, index)
        })
    )

const renderContent = () => {
  return (
      slots.default &&
      slots.default().find((item) => {
        if (currentTab.value === 0) {
          return true
        }
        return item.props?.tab === currentTab.value
      })
  )
}
</script>
<style scoped>
:deep(.tab-bar-button-list){
  font-size: 16px;
  display: flex;
  flex-wrap: nowrap;
  justify-content: space-around;
  align-items: center;
  padding: 5px 0;
  margin: 10px 20px;
  border-radius: 90px;
  background-color: rgb(245, 247, 251);
  overflow-y: hidden;
  overflow-x: auto;
 .tab-bar-button-item {
    flex: 1;
    padding: 3px 0;
    text-align: center;
    border-radius: 40px;
  }

  .tab-bar-button-item-active {
    background: #FB4624;
    color: white;
    transition: all 0.3s;
  }
}

</style>

tab.vue

<template>
  <div>
    <slot></slot>
  </div>
</template>

代码很简单,tabs组件接收两个props属性,nameinput单选框的属性,default 代表当前选中的是哪一个子tabtab 也接受两个props属性,nameinputvalue值,也就是要在顶部显示的文字,tab属性是标记当前tab组件的唯一标识,用来和 tabs组件的 default 属性判断是否相同,相同则添加被选中的样式。

index.vue 中使用

<template>
  <div class='content'>
   <Tabs :default=1>
      <tab name='第一页' :tab=1>
        测试1
      </tab>
      <tab name='第二页' :tab=2>
        测试2
      </tab>
    </Tabs>
  </div>
</template>

<script setup>
import Tabs from '@/components/Tabs/tabs.vue'
import Tab from '@/components/Tabs/tab.vue'

</script>

<style  scoped>
.content{
  font-size: 20px;
}
</style>

封装 Tab 组件,在组件中使用插槽时遇到的问题

可以看到,实现了正常的切换,另外,tab 插槽内使用组件也可以正常生效。

demo-child.vue

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

<template>
  <div>{{msg}}</div>
</template>

<style scoped>

</style>

index.vue 中使用

<template>
  <div class='content'>
    <Tabs :default=1>
      <tab name='第一页' :tab=1 >
        <DemoChild msg='组件测试1'></DemoChild>
      </tab>
      <tab name='第二页' :tab=2>
        <DemoChild msg='组件测试2'></DemoChild>
      </tab>
    </Tabs>
  </div>
</template>

<script setup>
import Tabs from '@/components/Tabs/tabs.vue'
import Tab from '@/components/Tabs/tab.vue'
import DemoChild from './demo-child.vue'
</script>

<style  scoped>
.content{
  font-size: 20px;
}
</style>

封装 Tab 组件,在组件中使用插槽时遇到的问题

但是,当我在demo-child 组件中使用插槽,将另一个组件放在 demo-child 的插槽内时,切换就失效了。

demo-child.vue 稍作修改

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

<template>
  <div>{{msg}}</div>
  <div style='color:red;'>
    <slot name='top'></slot>
  </div>
  <div style='color:green;'>
    <slot name='bottom'></slot>
  </div>
</template>

<style scoped>

</style>

demo-Grandson.vue

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

<template>
  <div>{{GrandMsg}}</div>
</template>

<style scoped>

</style>

index.vue 中使用

<template>
  <div class='content'>
    <Tabs :default=1>
      <tab name='第一页' :tab=1>
        <DemoChild msg='测试1'>
          <template #top>
            <DemoGrandson  GrandMsg='测试1的插槽放的组件'></DemoGrandson>
          </template>
        </DemoChild>
      </tab>
      <tab name='第二页' :tab=2>
        <DemoChild msg='测试2'>
          <template #bottom>
            <DemoGrandson  GrandMsg='测试2的插槽放的组件'></DemoGrandson>
          </template>
        </DemoChild>
      </tab>
    </Tabs>
  </div>
</template>

<script setup>
import Tabs from '@/components/Tabs/tabs.vue'
import Tab from '@/components/Tabs/tab.vue'
import DemoChild from './demo-child.vue'
import DemoGrandson from "./demo-grandson.vue"
</script>

<style  scoped>
.content{
  font-size: 20px;
}
</style>

封装 Tab 组件,在组件中使用插槽时遇到的问题

封装 Tab 组件,在组件中使用插槽时遇到的问题 这是为什么呢,观察切换时的 dom 结构即可发现,实际上切换时,只有一个 div 发生了改变,也就是说,实际上组件并没有进行销毁和重新挂载,而是进行了复用,这是因为 Vue 3 使用虚拟DOM来优化dom 的更新。当组件的数据变化时,Vue 会先更新虚拟 dom 树,然后通过 diff 算法找出最小必要变更,并应用这些变更到真实 dom 上。Vue 会尽量复用现有的组件实例和 dom 元素,以提高性能。如果两个组件具有相同的类型和相似的 props/stateVue 可能会复用这些组件的实例,而不是销毁旧实例并创建新实例。解决方式也很简单,只要给组件一个 key值,使组件更准确地识别哪些元素应该被保留、复用、移动或删除即可。

修改后的代码:

index.vue

<template>
  <div class='content'>
    <Tabs :default=1>
      <tab name='第一页' :tab=1>
        <DemoChild msg='测试1' :key='1'>
          <template #top>
            <DemoGrandson :key='1' GrandMsg='测试1的插槽放的组件'></DemoGrandson>
          </template>
        </DemoChild>
      </tab>
      <tab name='第二页' :tab=2>
        <DemoChild msg='测试2' :key='2'>
          <template #bottom>
            <DemoGrandson :key='2' GrandMsg='测试2的插槽放的组件'></DemoGrandson>
          </template>
        </DemoChild>
      </tab>
    </Tabs>
  </div>
</template>

<script setup>
import Tabs from '@/components/Tabs/tabs.vue'
import Tab from '@/components/Tabs/tab.vue'
import DemoChild from './demo-child.vue'
import DemoGrandson from "./demo-grandson.vue"
</script>

<style  scoped>
.content{
  font-size: 20px;
}
</style>

封装 Tab 组件,在组件中使用插槽时遇到的问题

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.