1
头图

原文参考我的公众号文章 微信小程序自定义动态tabBar

总结了一下小程序官方推荐的动态 tabBar 使用。通过自己的实践和落地实施,对使用动态 tabBar 时需要处理的问题(多角色不同 tabBar 动态渲染,页面加载后 tabBar 自动选中,操作权限判断,页面逻辑和 tabBar 初始化的先后关系控制。。。)进行了逻辑整合。

前置需求

  • 多角色
  • 动态tabBar
  • 操作权限控制

tabBar 完整代码和页面内的使用手法在文末,这里先分步看看这些问题当时都是如何处理的吧。

tabBar组件封装

组件结构的封装可参考官方代码,之后需要在组件js文件内对动态tabBar做处理即可;
import {
  geneAuthMenus,
  updateAppGlobalAuthData
} from './tabBarCtrl'
import {
  userLogin
} from '../models/UserApi'
import {
  showToast
} from '../utils/WxApi'
Component({
  data: {
    selected: 0,
    list: [],
  },
  methods: {
    switchTab(e) {
      const data = e.currentTarget.dataset
      const url = data.path
      wx.switchTab({
        url
      })
      this.setData({
        selected: data.index
      })
    }
  },
  lifetimes: {
    attached() {
      let appAuthMenus = [...getApp().globalAuth.menus];
      if (!getApp().globalAuth.received) {
        /**登录后App内记录 可访问页面、可操作动作 */
        userLogin({
          useCache: false
        }).then(res => {
          let {
            auth_list = [], action_list = [], role_id = 0, role_name = '小白'
          } = res?.data?.role_auth || {};
          let authList = geneAuthMenus(auth_list);

          updateAppGlobalAuthData({
            received: true,
            menus: authList,
            actions: action_list,
            roleId: role_id,
            roleName: role_name,
          });

          this.setData({
            list: authList
          })
        }).catch(err => {
          showToast(err.msg || '登陆失败')

          updateAppGlobalAuthData({
            received: true,
            menus: geneAuthMenus([]),
            actions: [],
            roleId: 0,
            roleName: '小白',
          });
        })
      } else {
        this.setData({
          list: appAuthMenus
        })
      }
    }
  }
})

tabBar 数据全局数据访问

App({
  globalAuth: {
    received: false, // 当状态变为true,代表auth相关数据已经拿到,与auth相关的逻辑现在可以执行了
    menus: [],
    actions: [],
    role_id: 0,
    role_name: '普通用户',
    roleHash: {
      0: '普通用户',
      1: '管理员',
      2: '运营',
      3: '维修师傅',
    }
  }
})

tabBar 数据来自【权限&menu】接口

custom-tab-barjs里,在lifetimesattached里调用和【权限&menu】相关接口,接口调用结束(无论成功或失败)后把getApp().globalAuth.received设为true
lifetimes: {
    attached() {
      let appAuthMenus = [...getApp().globalAuth.menus];
      if (!getApp().globalAuth.received) {
        /**登录后App内记录 可访问页面、可操作动作 */
        userLogin({
          useCache: false
        }).then(res => {
          let {
            auth_list = [], action_list = [], role_id = 0, role_name = '小白'
          } = res?.data?.role_auth || {};
          let authList = geneAuthMenus(auth_list);

          updateAppGlobalAuthData({
            received: true,
            menus: authList,
            actions: action_list,
            roleId: role_id,
            roleName: role_name,
          });

          this.setData({
            list: authList
          })
        }).catch(err => {
          showToast(err.msg || '登陆失败')

          updateAppGlobalAuthData({
            received: true,
            menus: geneAuthMenus([]),
            actions: [],
            roleId: 0,
            roleName: '普通用户',
          });
        })
      } else {
        this.setData({
          list: appAuthMenus
        })
      }
    }
  }

解决动态tabBar带来的问题

如何保证页面逻辑在【权限&menu】接口请求结束之后执行:通过访问全局状态字段getApp().globalAuth.received状态来判断;
// 通过无限轮询globalAuth.received状态的变化,监测到变化后再执行后续

export function doSthAfterDependentChangedPromise(computed = () => {}) {
  let loopTicker = null;
  const dependentTargetChanged = (resolver) => {
    if (getAppGlobalAuthData('received')) {
      console.log('doSthAfterDependentChangedPromise=>' + computed.getName())
      clearTimeout(loopTicker);
      resolver(computed());
    } else {
      loopTicker = setTimeout(() => {
        dependentTargetChanged(resolver)
      }, 200);
    }
  }

  return new Promise(resolve => {
    dependentTargetChanged(resolve)
  })
}
2.默认页面问题;

由于上面已经可以做到在【权限&menu】接口结束后再执行其他逻辑,那么就可以做到拿到 menu 数据后调用 wx.redirectTo 到角色默认的页面;(由于我的项目默认页面是一个所有角色公有的页面,所以就没做这方面处理)

tabBar页面自动更新 tab 索引

在使用tabBar的页面调用selectTabBar方法,因为方法核心是通过Page实例调用getTabBar方法,所以把this传递进去了。参考官方说法:如需实现 tab 选中态,要在当前页面下,通过 getTabBar 接口获取组件实例,并调用 setData 更新选中态。
onShow: function () {
   selectTabBar(this);
},
export function selectTabBar(context) {
  const computed = () => {
    let authMenus = [...getAppGlobalAuthData('menus')];
    let currentPath = getCurrentRoute();
    let pageIndex = authMenus.findIndex(item => item.pagePath.includes(currentPath));
    pageIndex = pageIndex == -1 ? 0 : pageIndex;

    if (typeof context.getTabBar === 'function' &&
      context.getTabBar()) {
      context.getTabBar().setData({
        selected: pageIndex
      })
      console.log('select current path:', currentPath)
    }
    return 1;
  }

  return doSthAfterDependentChangedPromise(computed)
}
3.含 tabBar 的页面超过了五个:小程序在 app.json 内通过 "tabBar" 字段定义了 list 只能有五项,所以可以把这些页面作为某一个 tabBar 页面的二级入口。

对代码进行整合

上述实现中,将tabBar相关数据挂载在App实例中,为了让tabBar组件相关功能更加紧密,逻辑更加清晰,所以把功能和数据整合到了一起,形成了 custom-tab-bar/model.js 文件。
function getCurrentRoute() {
  let route = '/pages/home/home'
  let pages = getCurrentPages();
  if (pages.length) {
    route = pages[pages.length - 1].route;
  }
  return route;
}

const PAGE_ENVIRONMENT = {
  "pagePath": "/pages/pkgStoreInspection/Environment/Environment",
  "text": "页面0",
  "iconPath": "/resources/icon/lubanya/tabbar/Env.png",
  "selectedIconPath": "/resources/icon/lubanya/tabbar/Env_cur.png"
}

const PAGE_NG = {
  "pagePath": "/pages/pkgStoreInspection/NG/NG",
  "text": "页面1",
  "iconPath": "/resources/icon/lubanya/tabbar/Nogood.png",
  "selectedIconPath": "/resources/icon/lubanya/tabbar/Nogood_cur.png"
}

const PAGE_INSPECTION = {
  "pagePath": "/pages/pkgStoreInspection/inspection/inspection",
  "text": "页面2",
  "iconPath": "/resources/icon/lubanya/tabbar/Store.png",
  "selectedIconPath": "/resources/icon/lubanya/tabbar/Store_cur.png"
}

const PAGE_RECORD = {
  "pagePath": "/pages/pkgStoreInspection/record/record",
  "text": "页面3",
  "iconPath": "/resources/icon/lubanya/tabbar/History.png",
  "selectedIconPath": "/resources/icon/lubanya/tabbar/History_cur.png"
}

const PAGE_MACHINE_EMULATOR = {
  "pagePath": "/pkgElse/pages/machineEmulator/machineEmulator",
  "text": "页面4",
  "iconPath": "/resources/icon/lubanya/tabbar/History.png",
  "selectedIconPath": "/resources/icon/lubanya/tabbar/History_cur.png"
}

const PAGE_USER = {
  "pagePath": "/pages/me/me",
  "text": "页面5",
  "iconPath": "/resources/images/tabbar/mine.png",
  "selectedIconPath": "/resources/images/tabbar/mine_active.png"
}

const AUTH_PAGE_HASH = {
  'PAGE_ENVIRONMENT': PAGE_ENVIRONMENT,
  'PAGE_NG': PAGE_NG,
  'PAGE_INSPECTION': PAGE_INSPECTION,
  'PAGE_RECORD': PAGE_RECORD,
  'PAGE_MACHINE_EMULATOR': PAGE_MACHINE_EMULATOR,
  'PAGE_USER': PAGE_USER,
}

/**
 * TabBar数据和行为控制的单例类
 */
let CreateSingletonTabBar = (function () {
  let instance = null;
  return function (roleId) {
    if (instance) {
      return instance
    }

    this.index = 0;

    this.roleNameHash = {
      0: '普通用户',
      1: '管理员',
      2: '运营',
      3: '维修师傅',
    }

    this.authData = {
      received: false,
      pages: [],
      actions: [],

      roleId: roleId,
      roleName: this.roleNameHash[roleId],
    }


    return instance = this;
  }
})()

/**记录auth接口请求是否已经结束 */
CreateSingletonTabBar.prototype.getReceive = function () {
  return this.authData.received;
}

/**获取有权限的pages */
CreateSingletonTabBar.prototype.getAuthPages = function () {
  return this.authData.pages;
}

/**获取有权限的actions */
CreateSingletonTabBar.prototype.getAuthActions = function () {
  return this.authData.actions;
}

/**通过AUTH_CODE生成符合小程序tabBar数据格式的authPages */
CreateSingletonTabBar.prototype.geneAuthPage = function (auth_list = []) {
  console.log('got auth_list:',auth_list)
  let pages = [];
  if (auth_list && auth_list.length) {
    auth_list.map((item, index) => {
      pages.push({
        index,
        ...AUTH_PAGE_HASH[item]
      });
    })
  } else {
    pages = [AUTH_PAGE_HASH['PAGE_ENVIRONMENT'], AUTH_PAGE_HASH['PAGE_USER']];
  }
  return pages;
}

/**更新内部tabBar相关数据 */
CreateSingletonTabBar.prototype.updateAuthData = function (objData = {}) {
  this.authData = {
    ...this.authData,
    ...objData
  };
}

/**选中tabBar:在含tabBar的页面内调用 selectTabBar(this) */
CreateSingletonTabBar.prototype.selectTabBar = function (context) {
  let that = this;
  const computed = () => {
    let authMenus = [...that.getAuthPages()];
    let currentPath = getCurrentRoute();
    let pageIndex = authMenus.findIndex(item => item.pagePath.includes(currentPath));
    pageIndex = pageIndex == -1 ? 0 : pageIndex;
    that.index = pageIndex;

    if (typeof context.getTabBar === 'function' &&
      context.getTabBar()) {
      context.getTabBar().setData({
        selected: pageIndex
      })
    }
    return 1;
  }

  return that.doSthAfterDependentChangedPromise(computed)
}

/**判断角色是否拥有某个action权限 */
CreateSingletonTabBar.prototype.checkAuthAction = function (act_code) {
  let that = this;
  let computedCheckAuthAction = () => {
    return that.authData.actions.includes(act_code)
  }
  return that.doSthAfterDependentChangedPromise(computedCheckAuthAction)
}

/**获取角色role_id */
CreateSingletonTabBar.prototype.getRoleId = function () {
  let that = this;
  let computedGetRoleId = () => {
    return that.authData.roleId
  }
  return that.doSthAfterDependentChangedPromise(computedGetRoleId)
}

/**如果某些逻辑需要在auth接口请求结束后执行,可以用此方法包装调用 */
CreateSingletonTabBar.prototype.doSthAfterDependentChangedPromise = function (computed = () => {}) {
  let loopTicker = null;
  let that = this;
  const dependentTargetChanged = (resolver) => {
    if (that.authData.received) {
      clearTimeout(loopTicker);
      resolver(computed());
    } else {
      loopTicker = setTimeout(() => {
        dependentTargetChanged(resolver)
      }, 200);
    }
  }

  return new Promise(resolve => {
    dependentTargetChanged(resolve)
  })
}

export const TBInstance = new CreateSingletonTabBar(0)

轻松使用!

custom-tab-bar 内实现动态 tabBar,主要代码在 lifetimes

import {
  userLogin
} from '../models/UserApi'
import {
  showToast
} from '../utils/WxApi'
import {
  TBInstance
} from './model'
Component({
  data: {
    selected: 0,
    list: [],
  },
  methods: {
    switchTab(e) {
      const data = e.currentTarget.dataset
      const url = data.path
      wx.switchTab({
        url
      })
      this.setData({
        selected: data.index
      })
    }
  },
  /**以上代码为官方示例所有 */
  lifetimes: {
    /**这里是动态tabBar的关键代码 */
    attached() {
      let appAuthMenus = [...TBInstance.getAuthPages()];
      if (!TBInstance.getReceive() || !appAuthMenus.length) {
        /**登录后TBInstance内记录tabBar相关数据,如:可访问页面、可操作动作... */
        userLogin({
          useCache: false
        }).then(res => {
          let {
            auth_list = [], action_list = [], role_id = 0, role_name = '普通用户'
          } = res?.data?.role_auth || {};
          let authList = TBInstance.geneAuthPage(auth_list);

          TBInstance.updateAuthData({
            received: true,
            pages: authList,
            actions: action_list,
            roleId: role_id,
            roleName: role_name,
          })

          this.setData({
            list: authList
          })
        }).catch(err => {
          console.log(err)
          showToast(err.msg || '登陆失败')

          TBInstance.updateAuthData({
            received: true,
            menus: TBInstance.geneAuthPage([]),
            actions: [],
            roleId: 0,
            roleName: '普通用户',
          });
        })
      } else {
        this.setData({
          list: appAuthMenus
        })
      }
    }
  }
})

实现 tab 选中

调用 selectTabBar 选中当前页面对应的 tab,不需要传递 index,因为不同角色即便拥有的相同页面,对应的索引也可能是不一样的,所以这个动态的索引放到了 selectTabBar 内部实现
import {
  TBInstance
} from '../../../custom-tab-bar/model'

Page({
  data: {},
  onShow: function () {
    // 选中当前页面对应的tabBar,不需要传递index,因为不同角色即便拥有的相同页面,对应的索引也可能是不一样的,所以这个动态的索引放到了selectTabBar内部实现
    TBInstance.selectTabBar(this);
  }
})

实现判断是当前角色否有某个 action 的权限

import {
  TBInstance
} from '../../../custom-tab-bar/model'

Page({
  data: {
    showAddNgBtn: false
  },
  onShow: function () {
    // 判断是否有ACT_ADD_NG操作权限
    TBInstance.checkAuthAction('ACT_ADD_NG').then(res => {
      this.setData({
        showAddNgBtn: res
      })
    })
  }
})

实现 tabBar 的初始化与页面逻辑同步执行

封装了 doSthAfterDependentChangedPromise 方法自动检测 tabBar 逻辑的执行情况,结束后才执行传入的代码逻辑
import {
  fetchShopsEnv
} from '../../../models/InspectionApi'
import {
  TBInstance
} from '../../../custom-tab-bar/model'
Page({
  data: {
    list: [],
  },
  onLoad: function () {
    TBInstance.doSthAfterDependentChangedPromise(this.getShopEnv)
  },
  onShow: function () {
    TBInstance.selectTabBar(this);
  },
  getShopEnv: function () {
    fetchShopsEnv().then(res => {
      this.setData({
        list: res.data
      })
    }).catch(err => {})
  }
})

Believer
47 声望5 粉丝

无法忍受尘世间的丑 便看不到尘世间的美