1.所需要的文件及结构
2.标签页组件的核心代码分解
先初始化各个文件
●index.html
●tabs.js
●pane.js
pane需要控制标签页内容的显示与隐藏,设置一个data:show,并且用v-show指令来控制元素;
当点击到这个pane对应的标签页标题按钮时,此pane的show值设置为true,否则应该是false,这步操作是在tabs组件上完成的,我们稍后再介绍。
既然要点击对应的标签页标题按钮。那应该有一个唯一的值来标识这个 pane, 我们可以设置个prop: name让用户来设置,但它不是必需的。如果使用者不设置,可以默认从0开始自动设置,这步操作仍然是tabs执行的,因为pane本身并不知道自己是第几个。除了name. 还需要标签页标题的prop: label, tabs 组件需要将它显示在标签页标题里。这部分代码如下:
上面的prop: label 用户是可以动态调整的,所以在pane初始化及label更新时,都要通知父组件也更新,因为是独立组件,所以不能依赖像bus.js或vuex这样的状态管理办法,我们可以直接通过this.$parent访问tabs组件的实例来调用它的方法更新标题,该方法名暂定为updateNav。注意,在业务中尽可能不要使用$parent来操作父链,这种方法适合于标签页这样的独立用件,这部分代码如下:
在生命周期mounted,也就是pane初始化时,调用一遍tabs的updateNav方法,同时监听了prop: label,在label更新时,同样调用。
剩余任务就是完成tabs.js组件。
首先需要把panc组件设置的标题动态谊染出来,也就是当pane触发tabs的updateNav方法时,更新标愿内容。我们先看一下这部分的代码:
getTabs是一个公用的方法,使用this.$children来拿到所有的pane组件实例。
需要注意的是,在methods里使用了有function 回调的方法时(例如遍历数组的方法forfEach),在回调内的this不再执行当前的Vue实例,也就是tabs组件本身,所以要在外层设置个_this =this的局部变量来间接使用this。如果你熟悉ES2015,也可以直接使用箭头函数=>。
遍历了每一个pane组件后,把它的label和name提取出来,构成一个Object并添加到数据navList数组里,后面我们会在template里用到它。
设置完navList数组后,我们调用了updateStatus方法,又将pane组件遍历了一遍,不过这时是为了将当前选中的tab 对应的pane组件内容显示出来,把没有选中的隐藏掉。因为在上一步操作里,我们有可能需要设置curentValue来标识当前选中项的name (在用户没有设置value时,才会自动设置),所以必须要遍历2次才可以。
拿到navList后,就需要对它用v-for指令把tab的标题泣染出来,并且判断每个tab当前的状态。这部分代码如下:
在使用v-for指令循环显示tab标题时,使用v-bind:class指向了一个名为tabCls的methods来动态设置class名称。因为计算属性不能接收参数,无法知道当前tab是否是选中的,所以这里我们才用到methods,不过要知道,methods 是不缓存的。
点击每个tab标题时,会触发handleChange方法来改变当前选中tab的索引,也就是pane组件的name.在watch选项里,我们监听了currentValue,当其发生变化时,触发updateStatus 方法来更新pane组件的显示状态。
以上就是标签页组件的核心代码分解。总结一下该示例的技术难点:使用了组件嵌套的方式,将一系列pane组件作为tabs组件的slot;tabs 组件和pane组件通信上,使用了$parent和$children的方法访问父链和子链:定义了prop: value和data: currentValuc,使用$emit('input')来实现 v-model的用法。
3.标签页组件的完整代码
●index.html
<!--入口页-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>标签页组件</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="app" v-cloak>
<tabs v-model="activeKey">
<pane label="标签一" name="1">
标签一的内容
</pane>
<pane label="标签二" name="2">
标签二的内容
</pane>
<pane label="标签三" name="3">
标签三的内容
</pane>
</tabs>
</div>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="pane.js"></script>
<script src="tabs.js"></script>
<script>
var app=new Vue({
el:'#app',
data:{
activeKey:'1'
}
})
</script>
</body>
</html>
●pane.js
//标签页嵌套的组件 pane
Vue.component('pane',{
name:'pane',
template:'\
<div class="pane" v-show="show">\
<slot></slot>\
</div>',
props:{
name:{
type:String
},
label:{
type:String,
default:''
}
},
data:function(){
return{
show:true
}
},
methods:{
updateNav(){
this.$parent.updateNav();
}
},
watch:{
label(){
this.updateNav();
}
},
mounted(){
this.updateNav();
}
})
●tabs.js
//标签页外层的组件 tabs
Vue.component('tabs',{
template:' \
<div class="tabs"> \
<div class="tabs-bar"> \
<div \
:class="tabCls(item)" \
v-for="(item,index) in navList" \
@click="handleChange(index)">\
{{item.label}} \
</div> \
</div> \
<div class="tabs-content"> \
<slot></slot> \
</div> \
</div>',
props:{
value:{
type:[String,Number]
}
},
data:function(){
return{
currentValue:this.value,
navList:[]
}
},
methods:{
tabCls:function(item){
return [
'tabs-tab',
{
'tabs-tab-active':item.name===this.currentValue
}
]
},
getTabs(){
return this.$children.filter(function(item){
return item.$options.name==='pane';
})
},
updateNav(){
this.navList=[];
var _this=this;
this.getTabs().forEach(function(pane,index){
_this.navList.push({
label:pane.label,
name:pane.name||index
});
if(!pane.name){
pane.name=index;
}
if(index==0){
if(!_this.currentValue){
_this.currentValue=pane.name||index;
}
}
});
this.updateStatus();
},
updateStatus(){
var tabs=this.getTabs();
var _this=this;
tabs.forEach(function(tab){
return tab.show=tab.name===_this.currentValue;
})
},
handleChange:function(index){
var nav=this.navList[index];
var name=nav.name;
this.currentValue=name;
this.$emit('input',name);
this.$emit('on-click',name)
}
},
watch:{
value:function(val){
this.currentValue=val;
},
currentValue:function(){
this.updateStatus();
}
}
})
●style.css
/*样式表*/
[v-cloak]{
display:none;
}
.tabs{
font-size:14px;
color:#657180;
}
.tabs-bar:after{
content:'';
display:block;
width:100%;
height:1px;
background:#dedde4;
margin-top:-1px;
}
.tabs-bar{
display:inline-block;
padding:4px 16px;
margin-right:6px;
background:#fff;
border:1px solid #d7dde4;
cursor:pointer;
position:relative;
}
.tabs-bar div{
float:left;
}
.tabs-tab-active{
color:#3399ff;
border-top:1px solid #3399ff;
border-bottom:1px solid #fff;
}
.tabs-tab-active:before{
content:'';
display:block;
height:1px;
background:#3399ff;
position:absolute;
top:0;
left:0;
right:0;
}
.tabs-content{
padding:8px 0;
}