需求

现在想要实现一个这样的效果:
image.png
开发者的开发姿势希望是这样的:

  1. <template>
  2. <div class="tab-demo">
  3. <i-tabs :default-active-id="curTab" @on-change="handleChange">
  4. <i-tab-panel tab="菜单1" id="1">content1 of tab pane</i-tab-panel>
  5. <i-tab-panel tab="菜单2" id="2">content2 of tab pane</i-tab-panel>
  6. <i-tab-panel tab="菜单3" id="3">content3 of tab pane</i-tab-panel>
  7. <span slot="extra" class="extrabtn" @click="jumpToHome()">主页</span>
  8. </i-tabs>
  9. </div>
  10. </template>
  11. <script>
  12. export default {
  13. data() {
  14. return {
  15. curTab: '3'
  16. };
  17. },
  18. methods: {
  19. handleChange(curtab, prevtab) {
  20. this.curTab = curtab.id;
  21. },
  22. jumpToHome() {
  23. // xxxx
  24. }
  25. }
  26. };
  27. </script>

即组件api为:
i-tabs:

  • default-active-id: 指定当前高亮的tab
  • on-change:接受用户的点击切换事件,通过this.curTab=item.id手动实现更新

i-tab-panel:

  • tab:当前tab的展示名字
  • id:和default-active-id对应,决定高亮的命中

slot:extra:

  • 在tab栏右侧的自定义渲染内容

设计

一步一步解决问题:

0、搭架子,将组件注册为全局组件
1、根据tab-panel渲染出静态的tab栏,名字、extra自定义都各归其位,id与default-active-id匹配高亮当前tab
=> tabs组件 通过this.$children可以拿到tab-panel组件实例(tab、id),维护一个navList渲染出tabBar
tabBar里高亮的元素就是activeId === item.id 实现的
2、响应用户的点击,高亮用户点击的tab
3、渲染当前高亮的tab内容(activeId所在的tabpanel自定义的内容 contentxxx of tab pane)

image.png

内容页的三个tabpane ,作为tabs里的children定义,在tabs组件内部使用slots接住的
1、tab-panel怎么渲染,渲染在tab组件的哪里
2、如何渲染?是全部一次性加载默认display:none 高亮时show高亮的tab,还是”异步加载”每次替换content的内容
==> 2.1 每次只展现一个tabpane。怎么实现其他的不见的?

way1、css的display控制 只有当前的才可见
这样的话实际上每次都是同步更改
一次性还是加载了 那么多的dom

way2、tabpane里控制是否渲染内容
决定当前tabpane是否渲染,不是由于tabs控制。是由tabpane来控制
tab-panel: 读取tabs上的activekey。如果与当前匹配就渲染(v-if)
===> 2.1.1 数据通信那块怎么搞, 同步activekey的更新

扩展需求

1、支持非受控组件使用模式 v-model

梳理依稀activekey(v-model) 和 defaultkey:

不管组件使用者是否传入v-model。
组件内部都要维护activekey变量
当用户传入的时候 只是会用activekey向外更新v-model的值

当有v-model特意指定的时候:
此时判断v-model的值是否与tabpane的key匹配,如果匹配那就默认高亮那一项 (同时更新内部的
如果没有匹配,那就看用户是否传入了defaultkey。

2、tabBar的菜单支持tab-panel传入插槽
2、tab支持滚动
当tabs宽度过小的时候 tabs会出现滚动
image.png

3、下面高亮线动画友好

image.png

现在的content是来自于 根据当前activeid是否与paneid匹配 由于pane组件决定是否渲染
所以如果都渲染的话会出现

  1. div.tabs
  2. div.tabs-tabpane
  3. div.tabs-tabpane

div.tabs-tabpane 即使是inline-block 也会出现 闪动的现象

这样是没有效果的

  1. <transition name="fade">
  2. <div class="zhidao-tabs-tabpane" v-if="canrender">
  3. <slot></slot>
  4. </div>
  5. </transition>

测试

总结一个最佳实践出来就好写很多了
其实没有我想的那么复杂

要测试的点:
1、当tabbar传入插槽
2、实践1非受控组件
3、实践2 受控组件

tabbar传入插槽

  1. <tab-pane tab="关注" id="1">
  2. <span slot="tab">
  3. <em>Tab bar1</em>
  4. </span>
  5. tab1
  6. </tab-pane>
  7. <tab-pane tab="粉丝" id="2">
  8. tab2
  9. </tab-pane>

当slot=”tab”时 tabbar的循环渲染里不是 输出 pane.name 而是考虑这个pane的插槽

  1. <div class="zhidao-tabs-nav">
  2. <!-- div上面绑定样式宽度、color等 -->
  3. <div :class="tabCls(item)" v-for="(item, index) in navList" @click="handleClick(index)">
  4. <template v-if="item.isSlot">
  5. # 怎么在这里得到item的插槽
  6. <!-- <slot slot="tab">1</slot> -->
  7. </template>
  8. <template v-else-if="item.tab">{{item.tab}}</template>
  9. </div>
  10. </div>
  11. <script>
  12. // this.getTabs 得到tabs下面的tabpane的实例
  13. this.getTabs().forEach((pane, index) => {
  14. this.navList.push({
  15. tab: pane.tab || index, // 用户没有传入name 则绑定index
  16. isSlot: pane.$slots.tab ? 1 : 0,
  17. id: pane.id
  18. });
  19. if (index === 0 && this.activeId === 'default') {
  20. this.activeId = pane.id;
  21. };
  22. });
  23. </script>

现在的问题是要把子的插槽 放在父里渲染

image.png

非受控实践

成功

受控实践

image.png

受控手动控制高亮失败

总结

拆分思路、思路的难点
实现过程中用到的vue特性
多方比较体会到的组件设计优良之处,以及随着一个功能的新增原始设计的优良带来的扩展性友好问题