【vue 学习】从零建立tabbar基本结构

代码地址: LearnVuejs/owntabbar at master · Nshcn/LearnVuejs · GitHub
视频讲解:https://www.bilibili.com/video/BV15741177Eh?p=119

建立tabbar基本结构

vue init webpack tabbar 初始化项目tabbar后,删除自带的helloworld组件及其相关的内容,当运行npm run dev 的时候,会显示一片空白,因为什么组件都没有。
在App.vue中建立tabbar基本结构

<template>
  <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>
  </div>
</template>

这个时候直接显示出来的文字和界面存在缝隙,这是默认的margin和padding导致的,为此我们可以设置基本样式来去除body的margin和padding,我们在src/assets文件夹中创建两个文件夹:img和css,在css中创建base.css样式文件

body{
	margin:0;
	padding:0;
}

需要在main.js中引入基本样式文件require('./assets/css/base.css') 才能让css生效,但是不推荐这么做,因为显得很混乱,因为main.js最终会将App.vue进行渲染出来,所以我们可以考虑直接在App.vue里面设置样式,但是不推荐直接把大量的基本样式写入到style标签中,我们可以通过特殊的语法来在style里引用基本样式表,在js文件中我们直接通过import引入文件即可,但是style标签内,我们需要在import之前加上@,语法如下:@import "assets/css/base.css";
在style内给tabbar设置基本样式,实现向下靠齐,文字居中等。

<style>
@import "assets/css/base.css";
#tab-bar{
  display: flex;
  background-color: #f6f6f6;

  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;

  box-shadow: 0 -1px 1px rgba(100,100,100,0.1);
}
.tab-bar-item{
  flex: 1;
  /*让文本居中对齐*/
  text-align: center;
  height: 49px;
}
</style>

其中父盒子设置display:flex,子盒子设置flex:1可以实现父盒子中的子盒子均等分布;
设置tab-bar为固定定位,固定到屏幕的下方,注意需要设置left:0,right:0,让盒子撑满下边;
给tab-bar-item设置height:49px,49px是经过验证比较合适的tab栏的高度。

将tabbar分离成组件

我们接下来将tab-bar分离出来成为一个tabbar组件,在components文件夹中创建一个tabbar文件夹(为什么不直接创建一个tabbar.vue组件呢?因为组件可能会很多,全部集中到一起会显得很混乱,可以将与tabbar有关的组件全部放到一个文件夹中),在tabbar文件夹中创建TabBar.vue组件
将App.vue中的内容抽离出来到TabBar

<template>
  <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>
  </div>
</template>

<script>
export default {
  name: "TabBar"
}
</script>

<style scoped>
#tab-bar{
  display: flex;
  background-color: #f6f6f6;

  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;

  box-shadow: 0 -1px 1px rgba(100,100,100,0.1);
}
.tab-bar-item{
  flex: 1;
  text-align: center;
  height: 49px;
}
</style>

在App.vue中使用组件TabBar

<template>
<div id="app">
  <tab-bar></tab-bar>
</div>
</template>

<script>
import TabBar from "./components/tabbar/TabBar";
export default {
  name: 'App',
  components: {
    TabBar
  }
}
</script>

<style>
@import "assets/css/base.css";

</style>

在assets/img文件夹下创建tabbar文件夹用来存放与tabbar组件有关的图片,在TabBar.vue组件中使用img标签引入图片。

继续分离出tab-bar-item组件

我们可以看到tab-bar里面写了很多个tab-bar-item,我们可以进一步分离组件,将tab-bar-item抽离成组件,在components/tabbar文件夹中创建Tab-Bar-Item.vue组件,同时在tab-bar组件中创建插槽,用来防止tab-bar-item

在tab-bar组件中设置插槽
<div id="app">
  <div id="tab-bar">
    <slot></slot>
  </div>
</div>

<div class="tab-bar-item">
  <img src="../../assets/img/tabbar/home.svg" alt="">
  <div>首页</div>
</div>

在app组件中使用tab-bar和tab-bar-item
<template>
  <div id="app">
    <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>
  </div>
</template>

但是我们可以注意到,这里的tab-bar-item写死了,全部都是同样的,为了能展示不同的tab-bar-item,我们在tab-bar-item里面也设置插槽,因为我们需要使用不同的img和text文本,因此我们设置两个具名插槽,分别命名为item-icon和item-text,之后在App.vue中即可根据需要使用所需的icon和text文本。

在tab-bar-item中设置两个具名插槽
<div class="tab-bar-item">
  <slot name="item-icon"></slot>
  <slot name="item-text"></slot>
</div>

在App.vue中根据需要使用
<div id="app">
  <tab-bar>
    <tab-bar-item>
      <img slot="item-icon" src="./assets/img/tabbar/home.svg" alt="">
      <div slot="item-text">首页</div>
    </tab-bar-item>
    <tab-bar-item>
      <img slot="item-icon" src="./assets/img/tabbar/category.svg" alt="">
      <div slot="item-text">分类</div>
    </tab-bar-item>
    <tab-bar-item>
      <img slot="item-icon" src="./assets/img/tabbar/shopcart.svg" alt="">
      <div slot="item-text">购物车</div>
    </tab-bar-item>
    <tab-bar-item>
      <img slot="item-icon" src="./assets/img/tabbar/profile.svg" alt="">
      <div slot="item-text">我的</div>
    </tab-bar-item>
  </tab-bar>
</div>

给tab-bar-item设置不同的状态

我们希望实现某个tab-bar-item被点击的时候呈现不同的颜色,例如当点击时呈现红色,不点击时呈现默认的黑色,为此我们需要增加一个插槽用来存放active状态时的icon图标,但是这样会同时显示两个插槽中的图片,我们可以使用v-if和v-else语句来标记使用其中一个插槽,不要忘了在data中设置一个变量isActive来标记状态是否激活。

<div class="tab-bar-item">
  <slot v-if="!isActive" name="item-icon"></slot>
  <slot v-else name="item-icon-active"></slot>
  <slot name="item-text"></slot>
</div>

<script>
export default {
  name: "TabBarItem",
  data() {
    return {
      isActive: true
    }
  }
}
</script>

同时我们不止希望icon图标在激活时发生变化,也希望text在激活时颜色也变成红色,可以考虑给text插槽绑定class属性

在TabBarItem.vue中给text绑定class
<div class="tab-bar-item">
  <slot v-if="!isActive" name="item-icon"></slot>
  <slot v-else name="item-icon-active"></slot>
  <slot :class="{active:isActive}" name="item-text"></slot>
</div>

在App.vue中使用所需要的内容替换掉对应的插槽
<tab-bar-item>
  <img slot="item-icon" src="./assets/img/tabbar/category.svg" alt="">
  <img slot="item-icon-active" src="./assets/img/tabbar/category_active.svg" alt="">
  <div slot="item-text">分类</div>
</tab-bar-item>

但是在实际使用的时候发现通过class绑定的text在激活时颜色不会改变,这是为什么呢?因为在App.vue中的item-text会直接替换掉掉TabBarItem中的item-text插槽,即包括:class="{active:isActive}" 这部分内容也被替换掉了,所以无法正常绑定样式,为此,我们可以不直接在text插槽中绑定样式,而是通过将text插槽置于一个绑定了class的div盒子之中,这样App.vue中的内容指挥替换div中的插槽部分,而不会影响外层的div。

<div :class="{active:isActive}">
  <slot name="item-text"></slot>
</div>

这里可能会有疑问,v-if和v-else同样是直接放在插槽里面啊,为什么它们不会因为被直接替换掉而失效呢?这里应该是原生的属性被替换,vue专有的属性进行编译渲染了,但是在实际开发中,我们很可能搞不清楚什么时候插槽中的属性就因为被直接替换而失效了,所以实际开发中建议给其他插槽也包装上div,在外层的div中添加添加属性。

<div class="tab-bar-item">
  <div v-if="!isActive"><slot name="item-icon"></slot></div>
  <div v-else><slot name="item-icon-active"></slot></div>
  <div :class="{active:isActive}"><slot name="item-text"></slot></div>
</div>s

设置路由

执行命令npm install vue-router —save安装vue路由组件(其中—save表示运行时依赖,例如像express这些模块是项目运行必须的即为运行时依赖,应该安装在dependencies节点下,以-save的形式安装;而devDependencies节点下的模块是我们在开发时需要的,比如项目中使用的gulp,压缩css、js的模块,这些模块在我们的项目部署后面时不需要的,所以我们可以使用-save-dev的形式安装)
在src文件夹下创建router文件夹,里面新建index.js文件,进行路由相关的配置

//引入vue-router和vue
import Vue from 'vue'
import VueRouter from 'vue-router'

//1. 通过Vue.use(插件),安装插件,底层是执行VueRouter.install方法
Vue.use(VueRouter)

//2. 创建VueRouter对象
//配置路由和组件之间的应用关系
const routes=[

]
const router=new VueRouter({
  // routes:[  
  //    存放路由和组件的应用关系
  // ]  
  routes
})

//3. 将router对象传入到Vue实例
export default router

接下来在main.js中引入router对象并挂载到Vue实例上

import Vue from 'vue'
import App from './App'
//系统会自动寻文件夹下的index文件,可以省略掉router下的index.js
import router from './router'//简化./router/index.js'

Vue.config.productionTip = false
new Vue({
  el: '#app',
  router,
  render: h => h(App)
})

接下来设置需要路由的页面,在src文件夹中创建一个views文件夹,在里面分别建立home、category、cart、profile文件夹,我们将每个页面相关的东西存放在其专属的文件夹下,然后再在各个文件夹下创建vue页面,如Home.vue、Cart.vue等(在实际开发中,我们会在home、category、cart、profile文件夹下创建新的components文件夹用来存放与当前页面相关的一些组件,而src目录下的components文件夹则往往是用来存放公共的组件)
设置好需要路由的页面后,需要在router/index.js文件中进行路由映射的配置,首先我们需要通过import将各个页面引入到index.js文件中

const Home = () => import('../views/home/Home')
const Cart = () => import('../views/cart/Cart')
const Category = () => import('../views/category/Category')
const Profile = () => import('../views/profile/Profile')

然后再配置路由对象中的映射关系,映射关系的配置格式为一个path对应一个component组件

//2. 创建路由对象
const routes = [
  {
    path: '',
    redirect: '/home'
  },
  {
    path: '/home',
    component: Home
  },
  {
    path: '/cart',
    component: Cart
  },
  {
    path: '/category',
    component: Category
  },
  {
    path: '/profile',
    component: Profile
  },
]

我们注意到第一个路由映射配置为

{
  path: '',
  redirect: '/home'
},

这是一个特殊的路由配置,因为默认情况下,进入网站后,我们往往希望默认显示home页面,而无需用户点击后才显示home,即为了让初次加载页面时就能定位到首页,我们需要重定向一下, 当网站上为上面的路径时立即重定向到下面的路径中。
接下来我们需要为tab-bar中的每个tab-bar-item设置监听click点击事件,当点击对应的item时,跳转到与其相关的页面。监听点击事件是每个tab-bar-item都要设置的,所以我们直接在TabBarItem.vue组件中进行监听

<div class="tab-bar-item" @click="itemClick">
...
</div>

配置itemClick方法,实现点击后跳转路径,可以使用this.$router.replace(path) 或者this.$router.push(path) ,两者的区别在于push方法产生的新页面会被添加到历史记录条目中,多次push相当于把url推进一个历史记录栈内,最后push的作为栈顶页面以显示,而replace是直接替换url, 不会把页面推入历史记录栈中

methods: {
  itemClick() {
    this.$router.replace(this.path)
  }
}

我们接着需要考虑的问题是方法中的路由从哪来呢?我们不能直接设定一个path写死路由,那样的话所有的item都是一个路由了,为此我们需要让不同的item传来自己需要跳转的路由,相比于直接在在TabBarItem里面设置所有item都要监听的点击事件,这次我们希望每个item传来的是自己的路由,因此,可以直接在App.vue中的tab-bar-item标签内设置传入自己的路由,注意因为传入的是字符串,所以我们无需通过v-bind绑定,直接使用path="/home"即可

<tab-bar-item path="/home">
  <img slot="item-icon" src="./assets/img/tabbar/home.svg" alt="">
  <img slot="item-icon-active" src="./assets/img/tabbar/home_active.svg" alt="">
  <div slot="item-text">首页</div>
</tab-bar-item>

然后在TabBarItem.vue中设置props接收路由参数

props: {
  path: String
},

我们需要添加router-view标签来显示我们需要跳转的路由页面,router-view相当于一个占位的东西,指定组件渲染到页面的哪个地方

<div id="app">
  <router-view></router-view>
  <tab-bar>...</tab-bar>
</div>

最后,因为vue-router中的默认路由配置方式是通过hash方式(通过Location.hash的方式修改url)配置的,因此url中会有#符号,我们可以修改index.js文件,在创建的vuerouter对象中修改路由默认路径配置方式,这样显示的url就是常见的url形式了

const router = new VueRouter({
  routes,
  mode:'history'
})

设置点击某个item时只有该item呈现激活状态的颜色

为了实现每次只有点击后的item显示红色,我们需要判断当前的item是否被点击处于活跃状态中,在之前的实现中,我们是在data属性中设置了isActive变量,使得所有的item都是同样的状态,如何能判断当前的item被激活呢?我们可以在computed设置一个isActive属性,因为每个item传来的path是自己固定的路径,而每次活跃的路由只有一个,因此我们判断当前活跃的路由是否含有当前item的路径,如果有,则说明当前item处于激活状态,返回true。

computed:{
  isActive(){
  //route是当前router路由对象实例
    return this.$route.path.indexOf(this.path)!==-1;
  }
}

实现自定义配置item激活时的颜色
之前我们在设置item中文本的颜色的时候,是通过直接绑定class属性:class={active:isActive} 当isActive为true的时候,文本显示成active对应的类的红色样式,但这种写法有个缺陷,因为我们有时候可能并不想要红色,而是其他颜色,而直接通过class绑定就写死了,为此我们可以考虑给文本动态绑定style,用户可以自己传入自己想要设置的颜色。我们先设置一个activeStyle计算属性,activeStyle仅当当前item为活跃状态时,返回一个样式。
在App.vue中传入我们想要设置的不同的样式

<tab-bar-item path="/cart" activeColor="blue">
  <img slot="item-icon" src="./assets/img/tabbar/shopcart.svg" alt="">
  <img slot="item-icon-active" src="./assets/img/tabbar/shopcart_active.svg" alt="">
  <div slot="item-text">购物车</div>
</tab-bar-item>

在TabBarItem.vue中通过props接收这个样式

props: {
  path: String,
  activeColor:{
    type:String,
    default:'red'
  }
}

设置一个计算属性activeStyle,当当前item激活时使用传入的样式

computed:{
  isActive(){
    return this.$route.path.indexOf(this.path)!==-1;
  },
  activeStyle(){
    return this.isActive?{color:this.activeColor}:{};
  }
},

通过v-bind绑定样式

<div :style="activeStyle"><slot name="item-text"></slot></div>

进一步抽象,将App.vue中的tab-bar标签的内容进一步封装

可以看到我们在App.vue中的代码比较多且杂,我们可以进一步封装将tab-bar整个内容提取成一个MainTabBar组件,从而在App.vue中直接使用main-tab-bar标签即可。
现在src/components文件夹下创建一个MainTabBar.vue组件,将App.vue中tab-bar标签整个复制到MainTabBar.vue组件中的template中,因为需要使用tab-bar和tab-bar-item组件,所以也需要在script标签内通过import引入

<template>
  <tab-bar>...
</template>

<script>
import TabBarItem from "./tabbar/TabBarItem";
import TabBar from "./tabbar/TabBar";

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

之后我们在App.vue中引入MainTabBar组件即可

<template>
  <div id="app">
    <router-view></router-view>
    <main-tab-bar/>
  </div>
</template>

<script>
import MainTabBar from "./components/MainTabBar";

export default {
  name: 'App',
  components: {
    MainTabBar
  }
}
</script>

在这里我们一定要注意一些路径的修改,例如图片或文件的引入路径需要更改,否则会报错

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值