Vue之Tab组件开发

需求分析

Tab不管在移动端或者在PC端都非常常见,今天尝试了一下,发现并没有想象那么简单,可能是框架复杂性,但一件事想做好它永远都不简单。

我们想要页面渲染出类似的DOM结构

1
2
3
4
5
6
7
<div>
<ul>
<li>title1</li>
<li>title1</li>
</ul>
<div class="content"></div>
</div>

我们的需求很简单, 可以根据tab选项卡显示不同的tab页内容。那么在Vue中我们应该如何实现这种结构呢?

我们把上面的代码设计成下面这种形式去实现

1
2
3
4
5
6
7
8
9
10
11
<tabs value="1">
<tab label="tab1" index="1">
<span>content1</span>
</tab>
<tab label="tab2" index="2">
<span>content2</span>
</tab>
<tab label="tab3" index="3">
<span>content3</span>
</tab>
</tabs>

tabs中value用于指定默认选中的Tab页面, tab中的label用于设置标题,index用于跟value中对应是否选中

代码实现

从上边的代码片段,我们分析出需要创建三个组件,父级为tabs.vue,子级为tab.vue和tab-container.vue

  • tabs.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
name: 'Tabs',
props: {
value: {
type: [String, Number],
required: true
}
},
render () {
return (
<div class="tabs">
<ul class="tabs-header">
{this.$slots.options}
</ul>
</div>
)
}
}
</script>

代码使用了jsx的写法,因为vue中的js是用babel处理的,我们可以用babel-plugin-transform-vue-jsx这个插件支持

我们将使用slot插槽去包含子组件tab.vue

  • tab.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
export default {
name: 'Tab',
props: {
index: {
type: [Number, String],
required: true
},
label: {
type: String,
default: 'tab'
}
},
computed: {
active () {
return false
}
},
render () {
const tab = this.$slots.label || <span>{this.label}</span>
const classNames = {
tab: true,
active: this.active
}
return (
<li class={classNames} >
{tab}
</li>
)
}
}
</script>

tab同样也是使用jsx的写法,active我们使用计算属性去判断是否选中,代码给了一个默认值false,它作用就是跟父组件中的value比较,相同则选中

1
2
3
4
5
computed: {
active () {
return this.data.value === this.index
}
},

DOM雏形在这里大概出来,我们还需要把它们以组件的形式挂载到Vue实例中

1
2
3
4
5
6
7
import Tabs from './tabs.vue'
import Tab from './tab.vue'
export default (Vue) => {
Vue.component(Tabs.name, Tabs)
Vue.component(Tab.name, Tab)
}
  • main.js(页面入口)
1
2
3
import Tabs from './component/tabs/index.js'
Vue.use(Tabs)

文档说了,如果插件是返回一个函数,它会被作为install的用法,将Vue参赛传入,并且被多次调用的时候该插件只会安装一次

provide高级属性应用

还记得前面我们通过this.$parent去获取父组件上的属性吗,如果需求变了需要再嵌套一层div,那this.$parent就指向了div,那就不妥了,有什么办法呢?provide,它可以实现我们的需求。

  • tabs.vue
1
2
3
4
5
6
...
provide () {
return {
value: this.value
}
}

这里要注意了,如果是provide:{}直接以对象的话, 是拿不到this,因为这个时候Vue还没被初始化成功。

  • tab.vue
1
2
3
4
5
6
7
...
inject: ['value'],
computed: {
active () {
return this.value === this.index
}
},

问题看似得到解决,其实不然,因为provide它并不能react,也就是我们如果需要value能够实现数据响应,这种方式是做不到。我们都知道Vue是如何实现双向数据绑定,同样,我们这里也可以这样处理。

  • tabs.vue
1
2
3
4
5
6
7
8
9
10
11
provide () {
const data = {}
Object.defineProperty(data, 'value', {
get: () => {
return this.value
},
enumerable: true
})
return {
data
}
  • tab.vue
1
2
3
4
5
6
inject: ['data'],
computed: {
active () {
return this.data.value === this.index
}
},

最后我们需要不同tab显示不同的内容页

tab-container.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
props: {
panes: {
type: Array,
required: true
}
},
render () {
const contents = this.panes.map(pane => {
return pane.active ? pane.$slots.default : null
})
return (
<div class="tab-content">
{contents}
</div>
)
}
}
</script>

this.panes数据从哪来,别急

-tabs.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data () {
return {
panes: []
}
},
render () {
return (
<div class="tabs">
<ul class="tabs-header">
{this.$slots.options}
</ul>
<tab-container panes={this.panes}></tab-container>
</div>
)
},
  • tab.vue
1
2
3
mounted () {
this.$parent.panes.push(this)
}

这里得this其实就是tab,也就是

  • 的集合,tab-container再通过判断选中的li去显示对应的内容页。

    想一想

    可能有些小伙伴看到这里会有些疑问,为什么在父级中不直接把tab-container的节点直接写到render中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    render () {
    const contents = this.panes.map(pane => {
    return pane.active ? pane.$slots.default : null
    })
    return (
    <div class="tabs">
    <ul class="tabs-header">
    {this.$slots.options}
    </ul>
    <div class="tab-content">
    {contents}
    </div>
    </div>
    )
    }

    我们整个组件都是用了slot插槽,因为它的灵活性和可扩展性,但它并不支持react,所以如果我们需要再内容页做一些数据绑定的操作,你会发现很奇怪的现象。我们通过把内容页抽到一个组件,通过props实现了数据的react。

    看这里,babel-plugin-transform-vue-jsx3.7.0这个版本你会发现数据同样没有react,点击其他选项卡之后数据才更新,笔者目前还不知道具体什么原因导致的,但降级到3.5.0,这个功能就完美实现了,如果小伙伴得知,可以在下面留言告知~

    最后,具体的源码实现我已经上传到github,欢迎ForkStar

    感谢您的阅读,本文由 lynhao 原创提供。如若转载,请注明出处:lynhao(http://www.lynhao.cn
    Q&A
    CSS工程化方案