Vue 组件 | 如何从零封装一个tabbar组件

如何从零封装一个 Vue 组件哩?

今天来封装一个 tabbar 组件,在 CSS 效果 | tab 选项卡 中讲了使用原生 JavaScript 进行开发的方法,采用的是面向过程的编程思想。今天来用 Vue 封装一个类似的 tabbar 组件。

这个 tabbar 效果在移动端很常见,下面是一些例子。

pdd
zfb
jianshu

组件封装是为了复用,换成大白话就是,同样的事情我不想做第二遍,节省出来的时间用来看动漫不香吗?

好的,那怎么偷懒呢,根据杠杆原理,想要后期偷懒,前期就要尽可能地多考虑到会出现的各种情况。

先分析上面的图,从中早找出他们的共性和特性。


放一起,对比

共性是一个 TabBar 组件包含若干 TabBarItem 组件,数目一般不超过 6 个不少于 2 个,每个 TabBarItem 中包含一个 icon 和一行 text。特性是每个 TabBarItem 包含的 icon 和 text 的具体值是不同的,可以使用插槽。瞎掰完毕。


不妨使用上图的 jianshu 作为测试,封装一下 TabBar 组件。
在开始莽代码前,准备一波。

前期准备: icon图准备,使用的是阿里巴巴矢量图标库,根据 jianshu 的 TabBar 截图来看需要准备 5*2 个 svg icon,这应该是美工的活,如果有美工的话。

准备的icon

灰色 icon 是默认展示的,红色 icon 是被点击激活后展示的。


思路分析:


组件示例

整个看到的是一个大组件,大组件里有并列的 item,即上图中黄框部分,而每个 item 又可以抽离成小组件。

大组件定义插槽,传入小组件。小组件定义插槽传入图标和文字。


下面进入激动人心的编码环节。

编码开始:
不妨先在 App.vue 中编写:

<template>
  <router-view></router-view>
  <div id="app">
    <div id="tab-bar">
      <div class="tab-bar-item">首页</div>
      <div class="tab-bar-item">关注</div>
      <div class="tab-bar-item">简书砖</div>
      <div class="tab-bar-item">消息</div>
      <div class="tab-bar-item">我的</div>
    </div>
  </div>
</template>

<style scoped>
  #tab-bar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: 49px;
    background-color: #f5efef;
  }
  .tab-bar-item {
    flex: 1;
    text-align: center;
  }
</style>
效果

主要进行的基础结构的编写,基础布局是 TabBar 固定到底部,TabBarItem 平均布局,文字居中。


不妨建个文件抽离主要逻辑components/tabbar/TabBar.vue

<template>
  <div id="tab-bar">
    <div class="tab-bar-item">首页</div>
    <div class="tab-bar-item">关注</div>
    <div class="tab-bar-item">简书砖</div>
    <div class="tab-bar-item">消息</div>
    <div class="tab-bar-item">我的</div>
  </div>
</template>
<script>
export default {
  name: "TabBar"
}
</script>

<style scoped>
#tab-bar {
  display: flex;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 49px;
  background-color: #f5efef;
}
.tab-bar-item {
  flex: 1;
  text-align: center;
}
</style>

与之对应 App.vue 就精简很多

<template>
  <div id="app">
    <router-view></router-view>
    <tab-bar>
      
    </tab-bar>
  </div>
</template>
<script>
import TabBar from '@/components/tabbar/TabBar'

export default {
  components: { TabBar }
}
</script>

<style scoped>

</style>
第一次抽离

好的我们完成了第一次组件抽离,将 App.vue 拆成了 App.vueTabBar.vue


下面进入激动人心的第二次抽离。
不妨将准备好的 icon 放到 assets/img/tabbar 文件夹下,对于 icon 的命名有两种,一种是灰色 icon 采用英文命名,红色 icon 在对应的英文命名的基础上添加 _active的后缀,便于区分。下面就将准备好的 icon 引入到 Tabbar.vue 文件中吧。

<template>
  <div id="tab-bar">
    <div class="tab-bar-item">
      <img src="~@/assets/img/tabbar/home.svg" />
      首页
    </div>
    <div class="tab-bar-item">
      <img src="~@/assets/img/tabbar/love.svg" />关注
    </div>
    <div class="tab-bar-item">
      <img src="~@/assets/img/tabbar/diamond.svg" />
      简书砖
    </div>
    <div class="tab-bar-item">
      <img src="~@/assets/img/tabbar/comment.svg" />
      消息
    </div>
    <div class="tab-bar-item">
      <img src="~@/assets/img/tabbar/profile.svg" />
      我的
    </div>
  </div>
</template>
<script>
export default {
  name: "TabBar"
}
</script>

<style scoped>
#tab-bar {
  display: flex;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 49px;
  background-color: #f5efef;
}
.tab-bar-item {
  flex: 1;
  text-align: center;
}

.tab-bar-item img {
  width: 24px;
  height: 24px;
}
</style>

明显,现在 TabBar.vue 文件也有些臃肿,下面进行第二次抽离即对 TabBar 进行抽离。

<template>
  <div id="tab-bar">
    <slot></slot>
  </div>
</template>
<script>
export default {
  name: "TabBar"
}
</script>

<style scoped>
#tab-bar {
  display: flex;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 49px;
  background-color: #f5efef;
}
</style>

不妨建个文件抽离主要逻辑 components/tabbar/TabBarItem.vue 并微调样式。

<template>
    <div class="tab-bar-item">
      <img src="~@/assets/img/tabbar/home.svg" />
      首页
    </div>
</template>
<script>
export default {
  name: "TabBarItem"
}
</script>

<style scoped>
.tab-bar-item {
  flex: 1;
  text-align: center;
  font-size: 14px;
}

.tab-bar-item img {
  width: 24px;
  height: 24px;
  margin-top: 3px;
  vertical-align: middle;
}
</style>

这时可以在 App.vue 中进行使用。

<template>
  <div id="app">
    <router-view></router-view>
    <tab-bar>
      <tab-bar-item></tab-bar-item>
      <tab-bar-item></tab-bar-item>
      <tab-bar-item></tab-bar-item>
      <tab-bar-item></tab-bar-item>
      <tab-bar-item></tab-bar-item>
    </tab-bar>
  </div>
</template>
<script>
import TabBar from '@/components/tabbar/TabBar'
import TabBarItem from '@/components/tabbar/TabBarItem'

export default {
  components: {
    TabBar,
    TabBarItem
  }
}
</script>

<style scoped>

</style>
文件结构确立

是不是有点内味了?此时的效果图如下。


效果图

好嘞,第二次抽离结束,今天组件的大致文件结构就是现在的样子了,如果需要更加个性化的操作,可能还会对其进行抽离,今天就先这样。

现在我们有三个文件,其中 App.vue 已经成为了测试文件,用来检验我们的组件,现在的重点是处理 TabBar.vueTabBarItem.vue


先缓一缓,将精力暂时先集中到 TabBarItem.vue 文件上,它是来处理单个 item 的,从上面的效果图来看,每个 item 都长得一样,很明显不符合我们的需求,不够个性化,明显我要传值进来,这就需要在 TabBarItem.vue 中定义一些插槽。

看图可以发现需要搞两个插槽,不妨分别给它们起名: item-icon item-text。

// TabBarItem.vue
<div class="tab-bar-item">
  <!-- <img src="~@/assets/img/tabbar/home.svg" />
  <div>首页</div> -->
  
  <slot name="item-icon"></slot>
  <slot name="item-text"></slot>
</div>

在测试文件 App.vue 里进行插值操作测试。

<tab-bar>
  <tab-bar-item>
    <img slot="item-icon" src="~@/assets/img/tabbar/home.svg" />
    <div slot="item-text">首页</div>
  </tab-bar-item>
  <tab-bar-item>
    <img slot="item-icon" src="~@/assets/img/tabbar/love.svg" />
    <div slot="item-text">关注</div>
  </tab-bar-item>
  <tab-bar-item>
    <img slot="item-icon" src="~@/assets/img/tabbar/diamond.svg" />
    <div slot="item-text">简书砖</div>
  </tab-bar-item>
  <tab-bar-item>
    <img slot="item-icon" src="~@/assets/img/tabbar/comment.svg" />
    <div slot="item-text">消息</div>
  </tab-bar-item>
  <tab-bar-item>
    <img slot="item-icon" src="~@/assets/img/tabbar/profile.svg" />
    <div slot="item-text">我的</div>
  </tab-bar-item>
</tab-bar>

经过一番倒腾,组件的外在已经有那么回事了。


外在美

现在组件的外在搞好了,是一个纸片人的状态,下面让它动起来,响应我们的交互。
功能点1:点击变色。

这时就要用到我们准备的另外一组图片了,在 item 被点击时,我们给它换张图片,再搞一个插槽 item-icon-active 用来存储 item 被激活的 icon 。

<!-- TabBarItem.vue -->
<div class="tab-bar-item">
  <slot name="item-icon"></slot>
  <slot name="item-icon-active"></slot>
  <slot name="item-text"></slot>
</div>

有了新的插槽,我们就可以在 App.vue 中测试了,这里拿首页进行举例。

<!-- App.vue -->
<tab-bar-item>
  <img slot="item-icon" src="~@/assets/img/tabbar/home.svg" />
  <img slot="item-icon-active" src="~@/assets/img/tabbar/home_active.svg" />
  <div slot="item-text">首页</div>
</tab-bar-item>

此时的效果图如下:


我都要

嗯,连 icon 都成双成对的。。。

我们的使命是破坏它们,只能显示一个,要么是灰色 icon 要么是红色 icon 。

当 item 处于激活状态是要换成红色的 icon,同时文字也要变成红色。

也就是说现在的任务是条件展示,会涉及到 v-if 和动态绑定 class 的相关知识。

<!-- TabBarItem.vue -->
<template>
  <div class="tab-bar-item">
    <!-- 两个插槽显示一个 -->
    <slot v-if="!isActive" name="item-icon"></slot>
    <slot v-else name="item-icon-active"></slot>
    <div :class="{active: isActive}">
      <slot name="item-text"></slot>
    </div>
  </div>
</template>
<script>
export default {
  name: "TabBarItem",
  data() {
    return {
      isActive: true
    }
  }
}
</script>

<style scoped>
.tab-bar-item {
  flex: 1;
  text-align: center;
  font-size: 14px;
}
.tab-bar-item img {
  width: 24px;
  height: 24px;
  margin-top: 3px;
  vertical-align: middle;
}
.active {
  color: #f40;
}
</style>

效果展示如下:


isActive

好的现在我们通过控制 isActive 的值来模拟 item 被激活的情况,通过 v-if 控制 icon 的切换,通过给文字动态添加类名并通过 CSS 来改变文字的颜色。

通过测试图可以发现一变全变,这个问题留在后面解决,肯定不能手动改 isActive 的值。


下面是第二个功能点:点击 item 跳转到对应的页面。

来人呀,上路由。

准备工作:编写各个组件,放到 views 文件夹中,下面是 Profile.vue 的示例代码。

<template>
  <div class="profile">
    <h1>cemcoe的页面</h1>
  </div>
</template>

下面是配置的路由表。

const routes = [
  {
    path: '',
    redirect: '/home'
  },
  {
    path: "/home",
    component: Home
  },
  {
    path: '/love',
    component: Love
  },
  {
    path: '/diamond',
    component: Diamond
  },
  {
    path: '/comment',
    component: Comment
  },
  {
    path: '/profile',
    component: Profile
  }
]

TabBarItem.vue 文件中去监听点击事件并进行调转功能的实现。

// 核心代码
this.$router.replace(this.path)
传值

App.vue 文件通过 path="/home" 将对应的跳转路径传给 TabBarItem.vue 通过监听组件的点击执行 itemClick() 跳转到对应的组件。

效果

第二个功能点实现。


目前功能点1还有一些问题,路由切换时,图片和文字没有改变,这是因为之前 isActive 是写死的,下面进行动态变化。

现在要解决的问题是如何知道当前哪个 item 处于活跃状态,知道之后我们就可以对 isActive 进行动态的赋值。这里要用到 this.$route

// TabBarItem.vue
computed: {
  isActive() {
    // return this.$route.path.indexOf(this.path) !== -1
    return this.$route.path.includes(this.path)
  }
}

这里使用 indexOf()includes 都是可以实现相应功能,但后者语义化更强。

收工

经过一番的倒腾,这个 TabBar 组件就封装好了。下面是一些边角料。


更加个性化的操作,活跃时字体的颜色可能并不是红色。我们可以动态设置。


动态传入颜色
非主流
更加复杂的场景

其实可以个性化的地方还有很多,如上图可能需要显示红点或者未读信息的数目等。比如整个大的背景色,TabBar 的位置可能在页面的上方区域。

本文是对 TabBar 组件的简单封装,具体哪些需要个性化可能需要考虑具体项目,个性化的地方也不是越多越好,暴露的接口越多,稳定性等方面可能会有一定程度的减低,维护成本也会增多,一句俗话:适合自己的才是最好的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容