这次我们要实现两个组件:Tab.vue 和 TabItem.vue,其中 TabItem 是 Tab 的插槽内容。实际上不用封装组件也能达到类似的效果,但是考虑到语义化和可复用性等要求,封装一个 Tabs 组件还是有必要的。
根据 Vue 的风格指南,组件名需为两个以上的单词,以避免和原生 HTML 的标签冲突,这里为了简单起见不实现这个要求。
Tabs 的基本功能
<Tab :active-index="activeIndex">
<TabItem
v-for="(tab, idx) in tabs"
:key="tab"
@tabItemClick="handleClick"
v-slot="{ index }"
>{{ tab }} - {{ index }}</TabItem>
</Tab>
- Tab 组件可以指定默认 active 的 TabItem
- TabItem 被点击后会 emit 一个 tabItemClick 事件通知自己被点击
- 能够使用 v-slot 获取 TabItem 的一些内容,比如该 Item 的 index
- 被点击的 TabItem 自动添加 active 样式
了解 h 函数
h 函数用来创建 VNode,所谓 VNode 实际上就是个原生 JS 对象,用来保存一些真实 HTML 元素的信息,如元素标签、属性、子元素等。操作 VNode 的性能远超操作原生 DOM。
h 函数的参数
h 函数的参数位置非常灵活,这里介绍两个最常用的
- h(元素类型, 元素的属性, 子元素)
- h(元素类型, 子元素)
例子
// <div class="title">Cap1</div>
h('div', {class: 'title'}, 'Cap1')
// <div class="title"><span>hello world</span></div>
h('div', {class: 'title'}, [
h('span', 'hello world')
])
// <span>lalalal</span>
h('span', 'lalalal')
在 setup 中使用 h 函数
使用渲染函数时无需写 template 标签,组件会被渲染成渲染函数实现的样式
export default {
setup(props, { emit, slots }) {
return () => h('div', 'h function')
}
}
// 等同于模板写法
// <template>
// <div>h function</div>
// </template>
需要注意的是,h函数是箭头函数的返回值,因为 setup 在每个组件创建时只调用一次
实现
Tab.vue
Tab 组件作为 TabItems 的包裹组件,应该提供一个插槽用来渲染 TabItems,同时需要保存一些状态(如当前活跃 tab 的下标)。
<script>
import { h } from 'vue';
export default {
// 需要声明 props,否则 setup 的 props 为 {}
// 因为 setup 中的 props 等同于选项式 api 的 props
props: {
activeIndex: Number
},
setup(props, { emit, slots }) {
// children 是插槽的所有子元素,也就是所有的 TabItems
const tabItems = slots.default ?
slots.default()[0].children :
[]
return () => h('div', tabItems.map((item, idx) => {
// 这里 item 实际也是个 VNode,使用 h 修改这些 VNode 的 props
return h(item, {index: idx, class: {
active: props.activeIndex === idx
}})
}))
}
}
</script>
TabItem.vue
<script>
import { h } from 'vue'
export default {
props: {
index: Number
},
emits: ['tabItemClick'],
setup(props, { emit, slots }) {
// 这里的插槽的内容是 TabItems 的内容
const slotItems = slots.default ?
slots.default({ index: props.index }) :
[]
return () => h('div', {
onClick: () => {
emit('tabItemClick', props.index)
}
}, slotItems)
}
}
</script>
<style scoped>
.active {
color:red;
}
</style>