我们要实现如下效果

单看效果似乎很简单,实则不然 首先我们的tab一般是这样的结构
<tabs>
<tab label="one">
//内容区
<div>
....
</div>
<tab>
</tabs>tabs是整个tab选项的容器,每个tab代表一个切换项,tab提供插槽用于展现当前tab的内容 似乎没有什么问题
但是我们之前写原生js就知道html渲染的tab是这样的
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
<ul>
<div>
this is content
</div>tab标签与tab内容是分离的,我们不可能这样做
<li>
one
<div> this is content</div>
<li>但是我们的组件确是如上的结构,这种方式应该怎么做,vue的组件也是从上到下执行的,我们不可能将其分离出来,通过本文你会知道如何去实现这样一个tab切换
新键tab主容器组件(tabs) /tab/tabs.vue
<script>
export default {
name :"Tabs",
props:{
//通过value控制显示哪个tab
value:{
type:[String,Number],
required:true
}
},
render(){
return (
<div class="tabs">
<ul class="tabs-header">
{this.$slots.default}
</ul>
</div>
)
},
data(){
},
methods:{
}
}
</script>
<style lang="stylus" scoped>
.tabs-header
display flex
list-style none
margin 0
padding 0
border-bottom 2px solid #ededed
</style>这里我们使用了render函数和jsx语法使我们的组件更加灵活,ul提供默认插槽放置标签
新键tab组件 /tab/tab.vue
<script>
export default {
name:"Tab",
props:{
index:{
required:true,
type:[Number,String]
},
label:{
type:String,
default:'tab'
}
},
computed:{
active(){
return false
}
},
methods:{
handleClick(){
}
},
render(){
//插槽 或者label
const tab = this.$slots.label || <span>{this.label}</span>
const classNames = {
tab:true,
active:this.active
}
return (
<li class={classNames} on-click={this.handleClick}>
{tab}
</li>
)
}
}
</script>
<style lang="stylus" scoped>
.tab
list-style none
line-height 40px
margin-right 30px
position relative
bottom -2px
cursor pointer
&.active
border-bottom 2px solid blue
&:last-child
margin-right 0
</style>该组件接收index和lable用于与父组件配合控制其激活状态lable设置标签文字,在render函数中我们展示标签内容,要不以插槽的方式要么以传值的方式
接下来我们在全局注册这两个组件 /tab/index.js
import Tabs from './tabs.vue'
import Tab from './tab.vue'
export default (Vue)=>{
Vue.component(Tabs.name,Tabs)
Vue.component(Tab.name,Tab)
}在入口文件引入
...
import Tabs from './components/tabs/index.js'
Vue.use(Tabs) //tab组件
...接下来我们可以在全局引用
<div class="tab-container">
<tabs value="1">
<tab label="tabs" :index="1"></tab>
<tab index="2">
<span slot="label">two</span>
</tab>
<tab label="tabs3" index="3">
<span>three</span>
</tab>
</tabs>
</div>
基本效果搭建完成,接下来我们来完成tab切换
首先我们先让标签之间能够切换 给tabs添加点击事件,当元素被点击的时候我们向外emit一个change事件,并将当前被点击的tab索引暴露出去
tabs.vue
...
methods:{
onChange(index){
this.$emit('change',index)
}
}
}
</script>
<style lang="stylus" scoped>
.tabs-header
display flex
list-style none
margin 0
padding 0
border-bottom 2px solid #ededed
</style>我们在外面监听change事件,并改变tabs的value值
...
<tabs :value="tabValue" @change="handleChangeTab">
<tab label="tabs" :index="1">
data(){
return {
tabValue:1
}
},
handleChangeTab(value){
this.tabValue = value
}
...写到这里我们还没有触发 onChange方法 我们在标签被点击的时候触发父元素的onChange方法 并将当前索引暴露出去
computed:{
active(){
// return false
//判断如果父元素的vaule与当前索引相同则为激活状态
return this.$parent.value === this.index
}
},
methods:{
handleClick(){
this.$parent.onChange(this.index)
}
},我们使元素处于激活状态的依据是标签索引与父元素value相同,当标签被点击时,我们会将当前索引暴露出去,同时时父元素的value等于当前被点击标签索引,这样即实现标签的选中激活。

接下来我们开发标签对应的内容展示部分 我们在填入与当前标签相关的内容是这样操作的
<tab index="2">
<span slot="label">two</span>
<span>two</span>
</tab>如果这样做,他被渲染成html是这样的
<li>
two
<span>two</span>
</li>这里以span举例不太恰当,但实际我们不会这样做,这样做也不合理,那怎么使我们组件以最简单的方式书写,而每次渲染的时候都是如下形式呢
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
<ul>
<div>
this is content
</div>将标签组件的$solts.label抽离出来即可 首先我们在父容器定义panes接收标签组件 tabs.vue
...
data(){
return {
panes:[]
}
},
...标签组件将自己添加到父组件panes数组 tab.vue
mounted(){
this.$parent.panes.push(this)
},tabs.vue遍历子元素拿到当前激活状态的tab,并进行展示
render(){
const contents = this.panes.map(pane=>{
return pane.active ? pane.$slots.default:null
})//获取当前激活状态的tab内容
return (
<div class="tabs">
<ul class="tabs-header">
{this.$slots.default}
</ul>
<div class="tab-container">
{contents}
</div>
</div>
)
}我们在进行全局调用
<div class="tab-container">
<tabs :value="tabValue" @change="handleChangeTab">
<tab label="tabs" :index="1">
<span>one</span>
</tab>
<tab index="2">
<span slot="label">two</span>
<span>two</span>
</tab>
<tab label="tabs3" index="3">
<span>three</span>
</tab>
</tabs>
</div>
到此基本完成