Easy to use wechat applet dynamic tabbar

Posted by OhLordy on Sat, 26 Feb 2022 03:25:38 +0100

Reference to my official account Wechat applet custom dynamic tabBar

Summarizes the use of dynamic tabBar officially recommended by the applet. Through their own practice and implementation, the problems that need to be handled when using dynamic tabBar (multi role and different tabBar dynamic rendering, tabBar automatic selection after page loading, operation permission judgment, page logic and tabBar initialization sequence control...) Logical integration.

Pre demand

  • Multi role
  • Dynamic tabBar
  • Operation authority control

The complete code of tabBar and the usage methods in the page are at the end of the article. Let's take a step-by-step look at how these problems were handled at that time.

tabBar component packaging

The encapsulation of component structure can refer to the official code, and then the dynamic tabBar needs to be processed in the component js file;

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) {
        /**After logging in, the accessible pages and operational actions are recorded in the App */
        userLogin({
          useCache: false
        }).then(res => {
          let {
            auth_list = [], action_list = [], role_id = 0, role_name = 'Xiaobai'
          } = 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 || 'Login failed')

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

tabBar global data access

App({
  globalAuth: {
    received: false, // When the status changes to true, it means that the auth related data has been obtained, and the auth related logic can now be executed
    menus: [],
    actions: [],
    role_id: 0,
    role_name: 'Ordinary users',
    roleHash: {
      0: 'Ordinary users',
      1: 'administrators',
      2: 'operate',
      3: 'Maintenance master',
    }
  }
})

tabBar data comes from the permission & menu interface

In the js of the custom tab bar, call the interface related to [permission & menu] in the attached of lifetimes. After the interface call is completed (whether successful or failed), put getapp() globalAuth. Set received to true

lifetimes: {
    attached() {
      let appAuthMenus = [...getApp().globalAuth.menus];
      if (!getApp().globalAuth.received) {
        /**After logging in, the accessible pages and operational actions are recorded in the App */
        userLogin({
          useCache: false
        }).then(res => {
          let {
            auth_list = [], action_list = [], role_id = 0, role_name = 'Xiaobai'
          } = 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 || 'Login failed')

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

Solve the problems caused by dynamic tabBar

How to ensure that the page logic is executed after the permission & menu interface request is completed: by accessing the global status field getapp() globalAuth. Judged by the received state;

// Global auth through infinite polling For the change of received status, follow-up will be carried out after monitoring the change

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. Default page problem;

Since the above can be done at the end of the permission &menu interface, and then execute other logic, then you can get the menu data and call wx.. Redirect to the default page of the role; (since the default page of my project is a page shared by all roles, I didn't do this)

The tab index is automatically updated on the page with tabBar

The selectTabBar method is called on the Page using tabBar. Because the core of the method is to call the getTabBar method through the Page instance, this is passed in. Refer to the official statement: if you need to realize the tab selected state, you should obtain the component instance through the getTabBar interface on the current Page and call setData to update the selected state.

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. There are more than five pages with tabBar: the applet is in app JSON defines that the list can only have five items through the "tabBar" field, so these pages can be used as the secondary entry of a tabBar page.

Integrate code

In the above implementation, the tabBar related data is attached to the App instance. In order to make the related functions of the tabBar component more compact and the logic clearer, the functions and data are integrated to form a custom tab bar / model JS file.

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": "Page 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": "Page 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": "Page 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": "Page 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": "Page 4",
  "iconPath": "/resources/icon/lubanya/tabbar/History.png",
  "selectedIconPath": "/resources/icon/lubanya/tabbar/History_cur.png"
}

const PAGE_USER = {
  "pagePath": "/pages/me/me",
  "text": "Page 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 Singleton class for data and behavior control
 */
let CreateSingletonTabBar = (function () {
  let instance = null;
  return function (roleId) {
    if (instance) {
      return instance
    }

    this.index = 0;

    this.roleNameHash = {
      0: 'Ordinary users',
      1: 'administrators',
      2: 'operate',
      3: 'Maintenance master',
    }

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

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


    return instance = this;
  }
})()

/**Record whether the auth interface request has ended */
CreateSingletonTabBar.prototype.getReceive = function () {
  return this.authData.received;
}

/**Get pages with permission */
CreateSingletonTabBar.prototype.getAuthPages = function () {
  return this.authData.pages;
}

/**Get actions with permission */
CreateSingletonTabBar.prototype.getAuthActions = function () {
  return this.authData.actions;
}

/**Through AUTH_CODE generates authPages that conform to the data format of the applet tabBar */
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;
}

/**Update internal tabBar related data */
CreateSingletonTabBar.prototype.updateAuthData = function (objData = {}) {
  this.authData = {
    ...this.authData,
    ...objData
  };
}

/**Select tabBar: call selectTabBar(this) in the page with tabBar */
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)
}

/**Judge whether the role has an action permission */
CreateSingletonTabBar.prototype.checkAuthAction = function (act_code) {
  let that = this;
  let computedCheckAuthAction = () => {
    return that.authData.actions.includes(act_code)
  }
  return that.doSthAfterDependentChangedPromise(computedCheckAuthAction)
}

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

/**If some logic needs to be executed after the auth interface request is completed, this method can be used to wrap the call */
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)

Easy to use!

The dynamic tabBar is implemented in the custom tab bar, and the main code is in 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
      })
    }
  },
  /**The above codes are all official examples */
  lifetimes: {
    /**Here is the key code of dynamic tabBar */
    attached() {
      let appAuthMenus = [...TBInstance.getAuthPages()];
      if (!TBInstance.getReceive() || !appAuthMenus.length) {
        /**After logging in, TBInstance records tabBar related data, such as accessible pages, operable actions */
        userLogin({
          useCache: false
        }).then(res => {
          let {
            auth_list = [], action_list = [], role_id = 0, role_name = 'Ordinary users'
          } = 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 || 'Login failed')

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

Implement tab selection

Call selectTabBar to select the tab corresponding to the current page. There is no need to pass the index, because even if different roles have the same page, the corresponding index may be different, so this dynamic index is implemented inside selectTabBar

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

Page({
  data: {},
  onShow: function () {
    // Select the tabBar corresponding to the current page, and there is no need to pass the index, because even if different roles have the same page, the corresponding index may be different, so this dynamic index is implemented inside the selectTabBar
    TBInstance.selectTabBar(this);
  }
})

The implementation determines whether the current role has permission for an action

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

Page({
  data: {
    showAddNgBtn: false
  },
  onShow: function () {
    // Determine whether there is ACT_ADD_NG operation authority
    TBInstance.checkAuthAction('ACT_ADD_NG').then(res => {
      this.setData({
        showAddNgBtn: res
      })
    })
  }
})

Realize the initialization of tabBar and the synchronous execution of page logic

It encapsulates the doSthAfterDependentChangedPromise method, which automatically detects the execution of tabBar logic, and executes the incoming code logic only after it is completed

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 => {})
  }
})

Topics: Mini Program