ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序动态tabBar实现:自定义组件+角色权限配置驱动

微信小程序动态tabBar实现:自定义组件+角色权限配置驱动 简介面向小程序开发者的动态tabBar实现方案基于custom-tab-bar自定义组件完全接管底部导航渲染解决不同用户角色需展示不同菜单、且菜单总数超过5个时原生tabBar难以满足的问题。包内提供7个导航项、2种权限的完整示例权限1显示1、2、3权限2显示4、5、6、7也可按需自由组合如1、3、5或2、4、6、7同时补充修改权限后需重新编译、高亮下标设置与首页跳转路径等关键细节方便复用到商家端、多角色管理等场景。压缩包共46个文件集合JSON页面配置、JS交互逻辑、WXSS样式、WXML页面结构及PNG图标整体仅17KB结构清晰便于按目录对照学习。已有3079人学习使用是动态tabBar场景下轻量且完整的参考范例。1. 需求拆解原生tabBar的边界在哪里先说结论微信小程序原生的tabBar确实做不到按用户角色动态切换更做不到超过5个菜单自由组合。为什么因为原生tabBar是写在app.json里的静态配置。小程序启动时框架会读取这份配置一次性渲染底部导航后续运行过程中你没法动态增删菜单项。哪怕你用wx.setTabBarItem去改文字、改图标、改红点也改不了菜单项的数量和结构——这玩意儿在app.json里钉死了一共几个tab哪些页面进tab启动那一刻就已经固定。而且原生tabBar还有一条硬限制list最多配置5项。超过5个开发工具直接报错压根不让你过。那用户角色不同、菜单不同这种需求怎么破业内常见的土办法有这么几个多套小程序不同角色用不同小程序菜单各配各的。维护成本爆炸用户还要来回切换体验割裂。一个tabBar页面里塞一堆入口所有角色共用一个tab结构不同角色的功能全部堆在首页或“我的”里靠页面内的列表去做区分。能做但底部导航就成了摆设用户体验很平庸。用自定义tabBar这也是我最推荐、也是本文要展开讲的方案——官方提供的custom-tab-bar能力配合配置驱动的菜单数据可以实现真正的“按角色动态渲染”而且菜单数量不受5个限制。多说一句很多人把“自定义tabBar”等同于“自己写一个底部导航组件”。其实不是。custom-tab-bar是微信官方提供的扩展机制你在项目根目录建一个custom-tab-bar文件夹里面放组件代码然后在app.json里开启custom: true框架就会用你的组件替代原生tabBar。关键在于——你的组件本质上是一个普通小程序组件它的数据是活的可以随时setData于是“动态”就变成了一个纯前端的数据问题。这就有意思了。原来被app.json锁死的限制一下子全部解开。菜单数量、菜单项权限、图标、选中态、甚至菜单项对应的跳转逻辑全部由你的业务代码说了算。2. 总体设计思路配置驱动 组件化2.1 配置驱动是这类需求的最优解动态tabBar的核心不是怎么写组件而是怎么设计菜单的“数据源”。我见过不少新手一上来就写死逻辑在tabBar组件里判断user.role admin然后渲染不同的菜单数组。这能跑但有个致命问题——角色一旦变多判断逻辑就会变成一团乱麻。今天加一个“运营”角色明天加一个“访客”角色你的tabBar组件里全是if-else谁维护谁想哭。正确的做法是配置驱动把菜单配置和角色权限抽离成纯数据组件只负责“根据配置渲染”完全不做业务判断。具体来说我维护一个菜单配置文件比如config/menus.js里面定义了所有可能的菜单项以及每个角色能看到哪些菜单// config/menus.js const ALL_MENUS [ { pagePath: /pages/home/index, text: 首页, icon: home, roles: [admin, user, guest] }, { pagePath: /pages/order/index, text: 订单, icon: order, roles: [admin, user] }, { pagePath: /pages/message/index, text: 消息, icon: message, roles: [admin, user] }, { pagePath: /pages/stats/index, text: 数据, icon: stats, roles: [admin] }, { pagePath: /pages/setting/index, text: 设置, icon: setting, roles: [admin] }, { pagePath: /pages/profile/index, text: 我的, icon: profile, roles: [admin, user, guest] } ] function getMenusByRole(role) { return ALL_MENUS.filter(item item.roles item.roles.includes(role)) } module.exports { getMenusByRole }这里每个菜单项都声明了可见角色getMenusByRole负责按角色过滤。以后要调整菜单权限改配置就行组件代码一行不用动。2.2 为什么选择custom-tab-bar而不是自绘组件肯定有人会问既然要动态我干脆不用tabBar能力自己写一个底部导航组件在每个页面底部引进去不是更灵活理论上是但实际会踩两个坑第一页面切换的“原生感”会打折。原生tabBar和wx.switchTab是深度绑定的切换tab时页面切换是原生栈级的。自绘组件如果用wx.redirectTo或wx.reLaunch去模拟tab切换页面栈会越积越深返回行为会变得不可控用户按返回键直接退出小程序的情况都可能出现。第二每个页面都要手动引入组件、传参。你可能有四五个tab页面每个页面都要处理“当前选中的是第几个tab”这份状态。改一处别处忘了同步就出bug。这种分布式状态管理特别容易翻车。custom-tab-bar方案则不一样。开启自定义后微信小程序会把tabBar的渲染和交互完全交给你但页面栈的切换机制仍然由框架管理。你的组件只负责“长什么样”和“点了之后调用什么API”不用关心tab页面之间的栈逻辑。状态只存在于组件自身天然集中。2.3 超过5个菜单怎么办这是更新版最多人关心的点。原生tabBar的5个限制本质上是为了防止菜单项过多导致点击区域太小、体验下降。自定义tabBar绕开了这个限制但没有绕开“体验问题”。所以我做的处理是菜单超过5个时通过左右滑动来扩展展示区域。第1到第5个正常显示多出来的菜单在左右两侧各放一个半显的提示区用户滑动或点击箭头可以滚动查看更多。滚动条区域隐藏在视觉之外保持底部导航的精简感。这个方案在真机上实测下来用户接受度挺高。因为底部导航本来就应该是“高频功能入口”超过5个本身就是产品层面该警惕的信号。技术上我给了你能力但产品上我还是建议你克制。3. 核心代码实现从app.json到custom-tab-bar3.1 app.json开启自定义tabBar模式先改app.json在tabBar配置里加一行custom: true。注意即使开了自定义模式tabBar.list仍然不能为空需要保留一份“默认菜单”作为兜底因为部分API和框架逻辑依赖这份配置{ pages: [ pages/home/index, pages/order/index, pages/message/index, pages/stats/index, pages/setting/index, pages/profile/index ], tabBar: { custom: true, color: #999999, selectedColor: #1AAD19, backgroundColor: #ffffff, list: [ { pagePath: pages/home/index, text: 首页 }, { pagePath: pages/order/index, text: 订单 }, { pagePath: pages/message/index, text: 消息 }, { pagePath: pages/stats/index, text: 数据 }, { pagePath: pages/profile/index, text: 我的 } ] }, usingComponents: {} }这里有个细节list里最多写5个但页面数组可以不止5个。开启custom后真正的渲染以custom-tab-bar组件为准list这份配置仅作为框架级的兜底存在。3.2 构建custom-tab-bar组件结构在项目根目录新建custom-tab-bar文件夹名字必须一字不差里面放四个文件index.js组件逻辑index.wxml组件模板index.wxss组件样式index.json组件配置index.json最简单{ component: true }index.js是整个动态tabBar的心脏。它要做三件事根据角色获取菜单配置、响应页面切换切换选中态、处理菜单项点击跳转。// custom-tab-bar/index.js const { getMenusByRole } require(../config/menus) Component({ data: { selected: 0, menus: [], tabBarHeight: 0, showScrollTip: false }, lifetimes: { attached() { // 从全局或缓存中获取当前用户角色 const role wx.getStorageSync(userRole) || guest this.initTabBar(role) } }, methods: { initTabBar(role) { const menus getMenusByRole(role) // 如果超过5个标记需要支持滑动 const showScrollTip menus.length 5 this.setData({ menus, selected: 0, showScrollTip }) // 如果超过5个给页面传一个标记用于样式微调 if (showScrollTip) { this.setData({ menus.length: menus.length }) } }, switchTab(e) { const { index, path } e.currentTarget.dataset if (index this.data.selected) return this.setData({ selected: index }) wx.switchTab({ url: path }) }, updateSelected(pagePath) { const { menus } this.data const idx menus.findIndex(item { return pagePath.includes(item.pagePath.split(/).pop()) }) if (idx ! -1) this.setData({ selected: idx }) } } })attached生命周期里组件一挂载就去拿角色、算菜单、渲染。角色可能来自全局变量、wx.getStorageSync缓存或者登录后自行调用initTabBar传进来。不同项目的角色来源不一样灵活处理就好。3.3 模板和样式让超过5个菜单滚动起来index.wxml的核心逻辑是遍历menus数组动态渲染每一个tab项。注意wx:for的key要用pagePath不要用索引因为菜单项会随角色变化索引不稳定。view classtab-bar {{showScrollTip ? tab-bar--scroll : }} scroll-view wx:if{{showScrollTip}} scroll-x enhanced show-scrollbar{{false}} classtab-bar__scroll view classtab-bar__inner view wx:for{{menus}} wx:keypagePath classtab-bar__item {{selected index ? tab-bar__item--active : }} >// pages/home/index.js Page({ onShow() { if (typeof this.getTabBar function this.getTabBar()) { this.getTabBar().updateSelected(this.route) } } })这就是updateSelected方法存在的意义。它根据当前页面路由查一下这个路由在菜单数组里的索引然后更新selected。注意页面和组件的通信必须通过this.getTabBar()这个实例方法来做。不要用事件总线去搞步骤多余且容易出乱子。每个tab页面都要写这一段生命周期代码建议抽一个behaviors或mixin复用避免5个页面重复粘贴。4. 角色绑定与动态刷新切换身份后tabBar如何自我更新4.1 登录流程中注入角色信息动态tabBar的前提是“知道当前用户是谁”。所以登录流程里拿到用户角色信息后必须立刻同步给tabBar组件。我的做法是在登录成功后把角色写入本地缓存然后调用一个全局的初始化方法// app.js App({ globalData: { userRole: }, onLaunch() { // 检查本地缓存 const role wx.getStorageSync(userRole) this.globalData.userRole role || }, setUserRole(role) { this.globalData.userRole role wx.setStorageSync(userRole, role) // 通知tabBar刷新 const pages getCurrentPages() if (pages.length 0) { const currentPage pages[pages.length - 1] if (typeof currentPage.getTabBar function currentPage.getTabBar()) { currentPage.getTabBar().initTabBar(role) } } } })setUserRole里getCurrentPages()取得当前页面栈拿到栈顶页面实例再通过getTabBar()拿到自定义tabBar组件调用它的initTabBar重新计算菜单。这里有一个特别容易踩的坑tabBar组件实例不是随时都能拿到的。某些时机比如onLaunch里直接调用getTabBar()会拿到undefined。所以代码里判空是必须的。如果当前页面栈为空就等tabBar组件自己挂载时再初始化——因为组件attached时本来就会读一次角色。4.2 角色切换的场景退出登录、切换账号动态tabBar最典型的场景是用户退出登录后以另一个角色登录底部导航要跟着变。我处理的方式是在退出登录的公共方法里清缓存、清全局角色、把tabBar重置回默认角色function logout() { const app getApp() app.globalData.userRole wx.removeStorageSync(userRole) // 重置tabBar为guest角色 const pages getCurrentPages() if (pages.length 0) { const currentPage pages[pages.length - 1] if (typeof currentPage.getTabBar function currentPage.getTabBar()) { currentPage.getTabBar().initTabBar(guest) } } wx.reLaunch({ url: /pages/login/index }) }注意这里用了wx.reLaunch而不是wx.redirectTo。因为退出登录属于“会话级跳转”需要把整个页面栈清掉避免用户按返回键退回上一个登录态页面。按下标切、按角色切都是这个套路。角色变了就重新调initTabBar组件内部会根据新角色重新过滤菜单。如果新菜单里没有用户当前所在的页面还需要把用户引导到新菜单的第一个tab页避免出现“当前页面不在tabBar里”的失控状态。4.3 极端场景同一页面多个角色都能访问还有一种需求偶尔会遇到同一个页面管理员和普通用户都能进但它在tabBar里的位置不同。比如“消息”页普通用户放在第3个tab管理员可能放在第4个tab。这种情况下updateSelected用pagePath.includes做模糊匹配就是坑——因为两个角色的菜单里都有这个页面你无法判断当前导航应该高亮第几个。我建议在这种场景下不要靠路由反推选中项而是页面在onShow时直接告诉tabBar“我应该选中哪个”// pages/message/index.js onShow() { if (typeof this.getTabBar function this.getTabBar()) { this.getTabBar().setData({ selected: 2 }) // 明确指定第几个 } }业务耦合度高但胜在简单可靠。反正tabBar组件本身不关心业务它只负责渲染你给的数据。5. 实测踩坑记录与性能细节5.1 图标资源路径问题自定义tabBar里图片路径不能以/开头直接引用绝对路径。如果你用image标签渲染图标src用/images/tab-home.png这种写法模拟器可能正常真机上会路径解析失败。正确做法是图标路径用相对路径或者干脆用网络图片、base64图标。如果必须用本地图建议把图片放在custom-tab-bar同目录下的images文件夹然后用相对路径../custom-tab-bar/images/tab-home.png引用。路径问题在真机上很坑务必提前测。5.2 selected状态在不同角色切换后串位如果管理员有6个菜单普通用户有4个菜单。管理员切到第6个菜单退出登录后普通用户登录tabBar重置为4个菜单——如果没有重置selected可能会出现“选中第5个tab”但tabBar压根没有第5个菜单的尴尬。解决方案是initTabBar里强制把selected归零initTabBar(role) { const menus getMenusByRole(role) this.setData({ menus, selected: 0, showScrollTip: menus.length 5 }) }5.3 页面滚动到顶部之后tabBar遮挡内容自定义tabBar和原生tabBar一样默认是fixed在底部的。但如果你在index.wxss里写了position: fixed; bottom: 0;页面内容可能被遮住。原生tabBar会自动给页面加padding-bottom自定义tabBar不会。所以要在每个tab页面的根节点上手动加一个等价高度的padding-bottom或者用env(safe-area-inset-bottom)兼容底部横条。5.4 真机上的滚动体验优化用scroll-view做横向滚动在安卓低端机上会有掉帧感。我给几个优化建议scroll-view开启enhanced属性启用增强渲染能力。scroll-x加上scroll-with-animation让切换更平顺。不要用transition动画去驱动tab项的位移动画scroll-view的滚动用原生滚动最流畅。图标统一用image标签且设置固定宽高避免渲染抖动。5.5 动态tabBar与分包页面的兼容如果你的tabBar里有分包页面要注意wx.switchTab的url必须写全路径包含分包前缀。而且分包页面作为tab页面时页面路径要写成分包内的路径菜单配置里的pagePath也要对应调整。这个细节容易在联调时才发现建议提前梳理菜单路径和分包结构。根据我个人经验动态tabBar方案上线后最容易出问题的往往不是代码逻辑而是图标资源路径和真机适配。切角色、刷菜单这些核心链路只要配置驱动设计合理基本不会出大问题。你自己实现的时候把菜单配置、角色过滤、选中态同步这三块拆干净后面扩展角色、增删菜单都会非常省心。本文还有配套的精品资源点击获取
返回列表