轻松使用微信小程序动态tabbar

1,451 阅读6分钟

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

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

前置需求

  • 多角色

  • 动态tabBar

  • 操作权限控制

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

tabBar组件封装

组件结构的封装可参考官方代码,之后需要在组件js文件内对动态tabBar做处理即可;

import { getAuthPages, 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 = getAuthPages(auth_list);

            updateAppGlobalAuthData({
              received: true,

              pages: authList,

              actions: action_list,

              roleId: role_id,

              roleName: role_name,
            });

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

            updateAppGlobalAuthData({
              received: true,

              pages: getAuthPages([]),

              actions: [],

              roleId: 0,

              roleName: "小白",
            });
          });
      } else {
        this.setData({
          list: appAuthMenus,
        });
      }
    },
  },
});

tabBar 数据全局数据访问

App({
  globalAuth: {
    received: false, // 当状态变为true,代表auth相关数据已经拿到,与auth相关的逻辑现在可以执行了

    pages: [],

    actions: [],

    role_id: 0,

    role_name: "普通用户",

    roleHash: {
      0: "普通用户",

      1: "管理员",

      2: "运营",

      3: "维修师傅",
    },
  },
});

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

custom-tab-barjs里,在lifetimesattached里调用和【权限&menu】相关接口,接口调用结束(无论成功或失败)后把getApp().globalAuth.received设为true

Component({
  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 = getAuthPages(auth_list);

            updateAppGlobalAuthData({
              received: true,

              pages: authList,

              actions: action_list,

              roleId: role_id,

              roleName: role_name,
            });

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

            updateAppGlobalAuthData({
              received: true,

              pages: getAuthPages([]),

              actions: [],

              roleId: 0,

              roleName: "普通用户",
            });
          });
      } else {
        this.setData({
          list: appAuthMenus,
        });
      }
    },
  },
});

解决动态tabBar带来的问题

1.如何保证页面逻辑在【权限&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) => {});
  },
});