微信小程序动态tabBar的几种实现方法

10,465 阅读7分钟

前言

在写公司项目时,遇到了一个需求: 用户登录后,如果这个用户有供应商身份,则在个人资料下显示切换按钮,点击切换后小程序底部的tabbar要变成其他的,接下来就跟大家分享下我的实现方式,欢迎各位感兴趣的开发者阅读本文。

自定义tabbar

刚开始求助别人时,他们说我这个需求需要通过自定义tabbar实现。

安装vant/weap

此处使用的tabbar使用vant提供的,所以需要安装下。

  • 在终端执行下述命令
  yarn add @vant/weapp --production
  • 如果你的小程序是npm构建方式的话,可参考vant官网提供的引入方式: 通过npm在项目中引入vant
  • 我的项目没有使用npm构建,所以采用本地安装的形式,按照下述操作进行,即可完成本地安装。
1. 在本地的npm库下找到我们通过yarn安装的vant-webapp包
2. 在项目的根目录下创建plguins文件夹,然后把第一步找到的vant-webapp文件夹复制到这里来
3. 安装完成,此时的目录结构是这样的

配置custom-tabbar

按照小程序官网的custom-tabbar所述,我们需要进行一些配置,才能让小程序识别到我们的自定义tabbar。

  • app.json中的tabbar项添加custom字段和vant的tabbar声明
// app.json
{
//   *********** 其他项省略 *******     //
"tabBar": {
    // custom设置为true,小程序就会项目目录找custom-tab-bar文件夹
    "custom": true,
    "color": "#CCCCCC",
    "selectedColor": "#1296db",
    "borderStyle": "black",
    "list": [
      {
        "pagePath": "pages/home/home",
        "text": "首页",
        "iconPath": "images/icon/vegetables.jpg",
        "selectedIconPath": "images/icon/vegetables_selected.png"
      },
      {
        "pagePath": "pages/shopping/shopping",
        "text": "购物车",
        "iconPath": "images/icon/shopping.png",
        "selectedIconPath": "images/icon/shopping_selected.png"
      },
      {
        "pagePath": "pages/user/user",
        "text": "我的",
        "iconPath": "images/icon/my.jpg",
        "selectedIconPath": "images/icon/my_selected.png"
      }
    ]
  },
  "usingComponents": {
    "van-tabbar": "/plguins/vant-weapp/dist/tabbar/index",
    "van-tabbar-item": "/plguins/vant-weapp/dist/tabbar-item/index"
  }
}
  • 在项目根目录创建custom-tab-bar文件夹,并添加对应的wxml、wxss、js、json文件
  • 编写 tabBar 代码
<!---wxml部分-->
<van-tabbar
  active="{{ active }}"
  bind:change="onChange"
>
  <van-tabbar-item icon="home-o">首页</van-tabbar-item>
  <van-tabbar-item icon="search">购物车</van-tabbar-item>
  <van-tabbar-item icon="friends-o">我的</van-tabbar-item>

</van-tabbar>
Page({
  data: {
    active: 0,
  },
  onChange(event) {
    this.setData({ active: event.detail });
    switch ( event.detail) {
      case 0:
        // 首页
        wx.switchTab({
          url:''
        });
        break;
      case 1:
        // 购物车
        wx.switchTab({
          url:''
        });
        break;
      case 2:
        // 我的
        wx.switchTab({
          url:''
        });
        break;
    }
  }
});

执行结果

本来我还满心欢喜的觉得,这个需求好简单,这么容易就实现了,结果编译后,我心态崩了,这用户体验也太差了吧... 我点击tabbar后,页面切换了,tabbar却没反应过来...

自定义组件

使用自定义tabbar形式实现,用户体验是很不好的,于是就只能找另一种解决方案了,经过一番查找后,答案指向了自定义组件,这种方案是比较麻烦的,需要将项目的原面进行重构,由Page方式改成Component形式。

实现过程

  • 我们还是使用vant提供的tabbar组件
  • 因为不需要微信提供的tabbar,所以删除掉app.json中的tabBar相关内容
  • 在page下创建main文件夹,并创建对应的小程序所需文件,如下图所示
  • 编写组件相关代码
<!-- main/index.wxml -->
<view class='main-wrapper' style='margin-bottom:{{tabbarHeight}}px;'>
  <Index wx:if='{{isLogin === false}}'></Index>
  <!--主页-->
  <home wx:if='{{activeIndex === 0 && isLogin===true && isSupplier===false}}' onShow="{{tabbar[0].selected}}"></home>
  <!--商品-->
  <commodity wx:if='{{activeIndex === 0 && isLogin===true && isSupplier===true}}' onShow="{{otherBar[0].selected}}"></commodity>
  <!--购物车-->
  <shopping wx:if='{{activeIndex === 1 && isLogin===true && isSupplier === false}}' onShow="{{tabbar[1].selected}}"></shopping>
  <!--订单-->
  <order wx:if='{{activeIndex === 1 && isLogin===true && isSupplier===true}}' onShow="{{otherBar[1].selected}}"></order>
  <!--我的-->
  <user wx:if='{{activeIndex === 2 && isLogin===true}}' onShow="{{tabbar[2].selected}}"></user>
</view>
<van-tabbar wx:if="{{isLogin === true && isSupplier === false}}" active="{{ activeIndex }}" bind:change="onChange" z-index="9999">
  <van-tabbar-item wx:for="{{tabbar}}" wx:key="index" info="{{item.tips}}">
    <image
            slot="icon"
            src="{{item.imgSrc}}"
            mode="aspectFit"
            style="width: 30px; height: 18px;"
    />
    <image
            slot="icon-active"
            src="{{item.selectImgSrc}}"
            mode="aspectFit"
            style="width: 30px; height: 18px;"
    />
    {{item.name}}
  </van-tabbar-item>
</van-tabbar>
<van-tabbar wx:if="{{isLogin === true && isSupplier === true}}" active="{{ activeIndex }}" bind:change="onChange" z-index="9999">
  <van-tabbar-item wx:for="{{otherBar}}" wx:key="index" info="{{item.tips}}">
    <image
            slot="icon"
            src="{{item.imgSrc}}"
            mode="aspectFit"
            style="width: 30px; height: 18px;"
    />
    <image
            slot="icon-active"
            src="{{item.selectImgSrc}}"
            mode="aspectFit"
            style="width: 30px; height: 18px;"
    />
    {{item.name}}
  </van-tabbar-item>
</van-tabbar>
// main/index.js
/*
因为此处小程序的授权登录页面也在main里进行引导,所以这个文件我们做了如下操作:
     1. 我们需要在app.js创建一个全局变量isLogin
     2. 在此处监听isLogin是否改变
     3. 如果改变就改变data中定义的isLogin属性,wxml页面就会对应的显示tabbar并进入home组件
*/
const app = getApp();

Page({
  data: {
    tabbar: [
      {
        name: "首页",
        tips:'',
        selected: false,
        imgSrc:"/images/icon/vegetables.jpg",
        selectImgSrc:"/images/icon/vegetables_selected.png"
      },
      {
        name: "购物车",
        tips:'',
        selected: false,
        imgSrc:"/images/icon/shopping.png",
        selectImgSrc:"/images/icon/shopping_selected.png"
      },
      {
        name: "我的",
        tips: '',
        selected: false,
        imgSrc:"/images/icon/my.jpg",
        selectImgSrc:"/images/icon/my_selected.png"
      }
    ],
    otherBar:[
      {
        name: "商品",
        tips: '',
        selected: false,
        imgSrc:"/images/icon/commodity.png",
        selectImgSrc:"/images/icon/commodity_selected.png"
      },
      {
        name: "订单",
        tips: '',
        selected: false,
        imgSrc:"/images/icon/order.png",
        selectImgSrc:"/images/icon/order_selected.png"
      },
      {
        name: "我的",
        tips: '',
        selected: false,
        imgSrc:"/images/icon/my.jpg",
        selectImgSrc:"/images/icon/my_selected.png"
      }
    ],
    tabbarHeight: app.isIPhoneX ? 84 : 50, // 底部tabbar高度
    activeIndex: 0,  // 选中的tab
    scrollTopArray: [], // 记录每个页面的滚动位置
    isLogin: app.globalData._isLogin,
    isSupplier: app.globalData._isSupplier,
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    this.data.tabbar.forEach((item, index, arr) => {
      this.data.scrollTopArray[index] = 0;
      // item.isFirstLoad = true
    });
    wx.setNavigationBarTitle({
      title: this.data.tabbar[0].name,
    })
  },

  //定义监听回调方法
  //app 监听回调方法
  watchBack: function (value) {  //这里的value 就是 app.js 中 watch 方法中的 set, 返回整个 globalData
    this.setData({
      isLogin: value._isLogin,
      isSupplier: value._isSupplier
    });
  },
  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    const self = this;
    this.updateSubPageShowHide(this.data.activeIndex);
    // 监听全局变量的改变
    getApp().watch(self.watchBack);
  },
  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {},

  onChange(event) {
    if (event.detail == this.data.activeIndex) return;
    this.updateSubPageShowHide(event.detail);
    this.setData({
      activeIndex: event.detail,
      pageName: this.data.tabbar[event.detail].name
    });
    // 还原子页面的滚动位置
    wx.pageScrollTo({
      duration: 0,
      scrollTop: this.data.scrollTopArray[event.detail]
    })
  },
  // 记录每个子页面的滚动位置
  onPageScroll(e) {
    this.data.scrollTopArray[this.data.activeIndex] = e.scrollTop;
  },
  // 更新组件的show hide 生命周期
  updateSubPageShowHide(currentIndex) {
    this.data.tabbar.forEach(function (value, i) {
      if (i == currentIndex) {
        value.selected = true;
        wx.setNavigationBarTitle({
          title: value.name,
        })
      } else {
        value.selected = false;
      }
    });
    this.setData({
      tabbar: this.data.tabbar,
    })
  },
});
// main/index.json, 此处用tabbar对应的组件声明
{
  "usingComponents": {
    "home":"/pages/home/home",
    "shopping": "/pages/shopping/shopping",
    "user": "/pages/user/user",
    "Index": "/pages/index/index",
    "commodity": "/pages/commodity/commodity",
    "order": "/pages/commodityOrder/commodityOrder"
  }
}
// app.js
/*
  由于main/index.js中使用了全局变量isLogin和isSupplier,我们需要做如下操作
      1. 在app.js中添加对应的全局变量
      2. 创建监听方法,监听这两个全局变量的改变

*/
  
  globalData: {
    userInfo: null,
    // 是否登录
    _isLogin:false,
    // 是否供应商
    _isSupplier:false,
    data:{

    }
  },
  watch: function (method) {
    let obj = this.globalData;
    Object.defineProperty(obj, "data", {  //这里的 data 对应 上面 globalData 中的 data
      configurable: true,
      enumerable: true,
      set: function (value) {  //动态赋值,传递对象,为 globalData 中对应变量赋值
        this._isLogin = value.isLogin;
        this._isSupplier = value.isSupplier;
        method(value);
      },
      get: function () {  //获取全局变量值,直接返回全部
        return this.globalData;
      }
    })
  }

// tabbar对应的页面改造成组件的示例
import base from "../../api/base";
var app = getApp();

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    height: {
      type: Number,
      value: app.homePageHeight
    },
    onShow: {
      type: Boolean,
      value: false,
      observer: 'onShowHideChange'
    },
  },

  /**
   * 页面的初始数据
   */
  data: {
    total: 0,
    size: 0,
    shoppingList: {},
    baseUrl:base.defaultBaseUrl
  },

  /**
   * 组件的方法列表
   */
  methods: {
    onShowHideChange(show) {
      if(show){
       // 组件显示时执行这里
        this.onShow();
      }else{

      }
    },
    },
    onShow: function () {
      this.initData();
    },
  }
});

执行结果

执行后,效果还不错,功能都实现了,但是还有一些遗留bug:

  • 切换后,状态不能保留。原因是在组件内我读不到全局变量设置的值,如果和page一样采用全局监听方式监听全局变量的改变,然后赋值到当前组件里,监听是监听到了,但是这个this指向的undefind,无法访问组件内的data数据。
  • 购物车的红点数量未显示,也是相同原因
  • 自动登录失效了
  • 身份切换后,点击新的tabbar选项,页面顶部的标题未变,这个问题稍微能好解决点,在main/index.js中切换组件时,加多个当前用的是哪个tabbar进行相应的个改变即可
  • 运行结果,如下图所示
    dfcb5362a7e784c4d220ad2be84ee93c.gif

setTabBarItem实现

当我带着自定义组件的形式遗留的问题去求助别人时,群友说我这个动态设置tabbar的需求通过微信官方的api就能实现,此刻我的内心拔凉拔凉的,昨天我咋就没发现这个解决方案呢,白折腾了一天,为了不让昨天的折腾白费于是大家就看到这篇文章的分享,也算是给自己的一个安慰。

8b63becc1338c0af1a014cbfa8884d53.png

然而,仔细观察微信提供的api后发现,这只能改变bar的图标和名字,不能改变其跳转的路径。那我要这个api有何用。

a010b62dc27e929e70b44083fe5320dd.png

我摊牌了,我搞不定了,谁行谁上吧,换一种方式实现吧。

d98e79599bb9ae61bf7eda7664b4dcbd.png
321fde23f04c0241197b5affe7280166.png
0e266bdb806e81aef21cd1000c5cdd84.png
25bc6f424b921cb865e9a4821ab10d35.png
d0e01d7d81531c977e23f2e349357cbe.png

写在最后

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载💌