原文见我的公众号文章 微信小程序自定义动态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-bar的js里,在lifetimes的attached里调用和【权限&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) => {});
},
});