需求分析
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
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
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) }
|
1 2 3
| import Tabs from './component/tabs/index.js' Vue.use(Tabs)
|
文档说了,如果插件是返回一个函数,它会被作为install的用法,将Vue参赛传入,并且被多次调用的时候该插件只会安装一次
provide高级属性应用
还记得前面我们通过this.$parent去获取父组件上的属性吗,如果需求变了需要再嵌套一层div,那this.$parent就指向了div,那就不妥了,有什么办法呢?provide,它可以实现我们的需求。
1 2 3 4 5 6
| ... provide () { return { value: this.value } }
|
这里要注意了,如果是provide:{}直接以对象的话, 是拿不到this,因为这个时候Vue还没被初始化成功。
1 2 3 4 5 6 7
| ... inject: ['value'], computed: { active () { return this.value === this.index } },
|
问题看似得到解决,其实不然,因为provide它并不能react,也就是我们如果需要value能够实现数据响应,这种方式是做不到。我们都知道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 }
|
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> ) },
|
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,欢迎Fork和Star。
感谢您的阅读,本文由
lynhao 原创提供。如若转载,请注明出处:lynhao(
http://www.lynhao.cn)