1. 项目概述与核心价值
最近在迭代一个后台管理类的小程序,产品经理拿着原型过来,说这次要支持多角色了。管理员、运营、审核员、普通用户,每个角色看到的底部导航栏(也就是tabBar)完全不一样。管理员得有“数据看板”和“系统设置”,审核员得天天盯着“待审列表”,而普通用户可能只看到“首页”和“我的”。这还没完,他还补了一句:“而且啊,以后可能还会加新角色,或者某个角色的tab数量可能会超过5个,咱们的架构得能灵活扩展。”
听到这里,我心头一紧。微信小程序原生的tabBar是静态配置在app.json里的,写死了图标、文字和页面路径,不仅无法根据登录用户动态变化,而且官方限制最多只能有5个tab。这需求明显是冲着原生tabBar的“命门”来的。如果硬着头皮用原生方案,难道要为每个角色单独建一个小程序?或者在一个小程序里写多套tabBar配置然后靠条件渲染?前者成本太高,后者在tab数量超过5个或者组合复杂时,代码会变成一团乱麻,维护起来简直是灾难。
所以,这个“动态tabBar”的需求,本质上是要我们自己动手,丰衣足食,抛弃原生tabBar,用自定义组件从头搭建一个完全受我们代码控制的导航栏。这样一来,tab的数量、内容、样式、显示逻辑就全部从配置文件中解放出来了,我们可以通过接口动态获取,也可以根据用户角色、权限甚至AB测试策略在运行时自由组合。这不仅是解决眼前多角色的问题,更是为小程序未来可能的复杂业务场景打下了一个灵活的基础。今天,我就把这次重构中基于自定义组件实现动态tabBar的完整方案、核心细节和踩过的坑,系统地分享给大家。
2. 整体架构设计与思路拆解
2.1 为什么必须放弃原生tabBar?
首先,我们得明确原生tabBar的局限性,这样才能理解自定义方案的必然性。
- 静态配置,无法动态修改:
app.json中的tabBar配置在小程序启动时就被读取并初始化,之后无法通过setData或任何API去修改它的list(即tab项)。这意味着你无法在用户登录后,再根据其角色去切换不同的tab。 - 数量硬性限制:官方明确规定,tabBar的list数组最多只能配置5个。对于某些拥有众多功能模块的管理后台,这个限制是无法接受的。
- 样式定制能力有限:虽然原生tabBar提供了一些基础样式配置,但对于一些高度定制化的UI设计(如特殊形状的选中态、复杂的动画效果),就显得力不从心。
- 与页面逻辑耦合度低:原生tabBar的切换是由小程序框架底层控制的,我们很难在其切换前后插入复杂的业务逻辑(例如,切换前检查表单是否已保存)。
基于以上几点,当我们的需求触及“动态”、“超限”、“深度定制”时,自定义组件方案就成了唯一优雅的解法。
2.2 自定义动态tabBar的核心设计思路
我们的目标是构建一个完全通过WXML、WXSS、JS逻辑控制的导航栏组件。其核心思路可以概括为:“一个组件,两处集成,动态数据驱动”。
- 一个组件:创建一个独立的自定义组件(比如
custom-tab-bar),它负责渲染所有tab的UI(图标、文字)并处理点击事件。 - 两处集成:
- 布局集成:在每个需要显示tabBar的页面底部,通过WXML标签引入这个自定义组件。这意味着它不再是全局的,而是页面的一部分。
- 逻辑集成:组件内部通过
properties接收外部传入的tab列表数据、当前选中索引等状态。页面JS逻辑负责计算和提供这些数据。
- 动态数据驱动:tab列表数据不再来自
app.json,而是来自一个可动态计算的数据源。这个数据源可以是:- 前端根据用户角色
role本地映射的一个配置对象。 - 从后端接口获取的个性化菜单配置。
- 本地存储
storage中保存的用户偏好设置。
- 前端根据用户角色
这样,只要改变传入组件的数据,tabBar的呈现内容就随之改变,完美实现了动态化。
2.3 技术选型与方案对比
在动手前,我们还需要考虑几个具体的技术选型点:
1. 组件通信:Properties vs. 全局状态管理
- Properties传参:简单直接,父页面(或任何一个引入该组件的页面)通过
data设置tabList,然后传递给组件。适用于tab配置相对固定,或只在少数页面变化的场景。 - 全局状态管理(如Vuex/Mobx理念):对于中大型项目,多个页面都需要共享和响应同一个tabBar状态时,可以使用小程序的
getApp().globalData,或者更优雅地使用像westore、mobx-miniprogram这样的状态管理库。组件监听全局状态,状态一变,所有页面的tabBar自动更新。实操心得:对于大多数动态tabBar场景,初期用
properties传参完全够用,结构清晰。只有当你的tabBar状态需要在数十个页面间复杂同步时,才考虑引入全局状态管理,避免过度设计。
2. 路由跳转:wx.switchTab 的陷阱这是最大的一个坑。原生tabBar页面必须使用wx.switchTab进行跳转,但当我们使用自定义tabBar后,底部的“页面”实际上已经不是原生tab页了。如果你继续用wx.switchTab,小程序会尝试去寻找原生tab配置,导致跳转失败或白屏。
- 正确做法:全部改用
wx.navigateTo或wx.redirectTo进行页面跳转。这意味着,所有通过自定义tabBar跳转的页面,在app.json的pages数组中,都应该是普通页面,而非tabBar中配置的页面。
3. 选中态与页面栈管理自定义tabBar没有原生的页面栈管理能力。我们需要自己维护一个currentIndex来指示当前选中的是哪个tab。当点击一个tab时,不仅要跳转到对应页面,还要更新这个currentIndex,并可能还需要考虑: - 如果目标页面已经在页面栈中,是跳转过去,还是用wx.navigateBack回退到它? - 如何让组件感知到用户通过浏览器导航栏或安卓返回键切换了页面,从而同步更新选中态? 这部分是自定义方案中逻辑最复杂的地方,我们会在后面详细展开。
3. 核心细节解析与实操要点
3.1 自定义组件的创建与基础结构
首先,我们在项目根目录下创建组件文件夹components/custom-tab-bar。
1. 组件JSON配置 (custom-tab-bar.json):
{ "component": true, "usingComponents": {} }这里注意,自定义tabBar组件本身一般不需要再引用其他子组件。
2. 组件WXML模板 (custom-tab-bar.wxml):这是UI渲染的核心。我们采用flex布局来平均分配tab项。
<view class="custom-tab-bar"> <block wx:for="{{tabList}}" wx:key="pagePath"> <view class="tab-item {{currentIndex === index ? 'active' : ''}}" >.custom-tab-bar { display: flex; position: fixed; bottom: 0; left: 0; right: 0; height: 100rpx; /* 高度可根据设计稿调整 */ background: #ffffff; border-top: 1rpx solid #f0f0f0; z-index: 999; /* 适配iPhoneX等刘海屏底部安全区域 */ padding-bottom: env(safe-area-inset-bottom); box-sizing: border-box; } .tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; } .tab-icon { width: 48rpx; height: 48rpx; margin-bottom: 4rpx; } .tab-text { font-size: 20rpx; color: #666666; } .tab-item.active .tab-text { color: #07c160; /* 选中态颜色 */ font-weight: bold; } .tab-badge { position: absolute; top: 8rpx; right: calc(50% - 20rpx); min-width: 32rpx; height: 32rpx; line-height: 32rpx; border-radius: 16rpx; background: #ff3b30; color: #ffffff; font-size: 20rpx; text-align: center; padding: 0 8rpx; box-sizing: border-box; }注意事项:
padding-bottom: env(safe-area-inset-bottom);这一行至关重要,它确保了在iPhone等具有底部安全区域的设备上,tabBar的内容不会被遮挡,提升用户体验。
4. 组件JS逻辑 (custom-tab-bar.js):
Component({ properties: { // 接收父页面传入的tab配置列表 tabList: { type: Array, value: [] }, // 接收当前选中的索引 currentIndex: { type: Number, value: 0 } }, data: { // 组件内部数据 }, methods: { // tab项点击事件 onTabItemTap(e) { const { index, path } = e.currentTarget.dataset; const currentPage = getCurrentPages().pop(); // 获取当前页面实例 // 1. 如果点击的就是当前已选中的tab,通常不做任何操作(或可以设计为刷新当前页) if (index === this.properties.currentIndex) { // 可选:触发当前页面的刷新逻辑,例如重新拉取数据 // currentPage.onTabItemTap && currentPage.onTabItemTap(); return; } // 2. 更新组件内部的选中状态(通过触发父页面的事件来更新,保持数据单向流) this.triggerEvent('tabchange', { index, path }); // 3. 进行页面跳转逻辑(建议在父页面监听tabchange事件后执行,这里仅作演示) // 重要:必须使用 wx.navigateTo 或 wx.redirectTo,而非 wx.switchTab // 因为自定义tabBar下的页面都是普通页面 this._navigateToPage(path, index, currentPage); }, // 私有的页面跳转方法 _navigateToPage(path, targetIndex, currentPage) { // 策略1:简单跳转,每次都打开新页面 // wx.navigateTo({ url: path }); // 策略2:智能路由,如果目标页面已在页面栈中,则回退到该页面 const pages = getCurrentPages(); const targetPageInstance = pages.find(page => `/${page.route}` === path); if (targetPageInstance) { // 计算需要回退的步数 const delta = pages.length - pages.indexOf(targetPageInstance) - 1; wx.navigateBack({ delta }); } else { // 页面不在栈中,正常跳转 wx.navigateTo({ url: path }); } } } })组件逻辑的核心是onTabItemTap方法。它做了三件事:1. 防止重复点击当前tab;2. 向父组件触发tabchange事件,通知选中项变化(遵循数据单向流);3. 执行页面跳转。我将复杂的“智能路由”逻辑封装成了_navigateToPage私有方法,这是一个提升体验的关键点。
3.2 动态数据源的设计与角色映射
tabBar的动态性来源于数据。我们需要一个中心化的地方来管理不同角色对应的tab配置。
1. 创建Tab配置管理器 (utils/tabConfig.js):
// 定义所有可能的tab项元数据 const ALL_TABS_META = { home: { text: '首页', iconPath: '/assets/icons/home.png', selectedIconPath: '/assets/icons/home-active.png', pagePath: '/pages/home/index' }, dashboard: { text: '数据看板', iconPath: '/assets/icons/dashboard.png', selectedIconPath: '/assets/icons/dashboard-active.png', pagePath: '/pages/dashboard/index', requiredRole: ['admin'] // 只有管理员角色可见 }, task: { text: '任务中心', iconPath: '/assets/icons/task.png', selectedIconPath: '/assets/icons/task-active.png', pagePath: '/pages/task/index', requiredRole: ['admin', 'operator'] }, audit: { text: '审核列表', iconPath: '/assets/icons/audit.png', selectedIconPath: '/assets/icons/audit-active.png', pagePath: '/pages/audit/list', requiredRole: ['auditor'] }, message: { text: '消息', iconPath: '/assets/icons/message.png', selectedIconPath: '/assets/icons/message-active.png', pagePath: '/pages/message/index', badge: 0 // 初始角标数,可从接口更新 }, profile: { text: '我的', iconPath: '/assets/icons/profile.png', selectedIconPath: '/assets/icons/profile-active.png', pagePath: '/pages/profile/index' }, // 可以继续添加更多tab项,不受5个限制 settings: { text: '设置', iconPath: '/assets/icons/settings.png', selectedIconPath: '/assets/icons/settings-active.png', pagePath: '/pages/settings/index', requiredRole: ['admin'] } }; // 角色到tab列表的映射关系(也可以从后端接口获取) const ROLE_TAB_MAP = { 'admin': ['home', 'dashboard', 'task', 'message', 'profile', 'settings'], // 管理员有6个tab 'operator': ['home', 'task', 'message', 'profile'], // 运营有4个 'auditor': ['home', 'audit', 'message', 'profile'], // 审核员有4个 'user': ['home', 'message', 'profile'] // 普通用户有3个 }; /** * 根据用户角色获取对应的tabBar配置列表 * @param {string} role - 用户角色标识 * @param {object} extraData - 额外数据,用于更新角标等动态信息 * @returns {Array} 过滤和组装后的tab配置数组 */ function getTabListByRole(role, extraData = {}) { const tabKeys = ROLE_TAB_MAP[role] || ROLE_TAB_MAP['user']; // 默认使用普通用户配置 return tabKeys.map(key => { const tab = { ...ALL_TABS_META[key] }; // 浅拷贝,避免污染元数据 // 注入动态数据,例如更新消息角标 if (extraData.messageCount !== undefined && key === 'message') { tab.badge = extraData.messageCount; } return tab; }); } /** * 根据当前页面路径,找出其在tabList中的索引 * @param {string} currentPath - 当前页面路径,如 '/pages/home/index' * @param {Array} tabList - 当前的tab配置列表 * @returns {number} 索引,未找到返回0 */ function getCurrentTabIndex(currentPath, tabList) { const index = tabList.findIndex(tab => tab.pagePath === currentPath); return index >= 0 ? index : 0; // 默认选中第一个 } module.exports = { ALL_TABS_META, ROLE_TAB_MAP, getTabListByRole, getCurrentTabIndex };这个设计非常清晰:
ALL_TABS_META:定义了所有可能的tab项,是“原料库”。ROLE_TAB_MAP:定义了不同角色能看到的tab项组合。getTabListByRole:核心函数,根据角色从原料库中取出对应的tab项,并可以注入实时数据(如未读消息数)。getCurrentTabIndex:辅助函数,用于页面初始化时,根据当前页面路径高亮正确的tab。
2. 在App或页面中集成:通常,我们在用户登录成功后,获取其角色信息,然后调用getTabListByRole生成tabList,并存入全局变量或当前页面的data中。
// 在app.js的onLaunch或登录成功后 const { getTabListByRole } = require('./utils/tabConfig'); App({ onLaunch() { // 模拟登录后获取用户信息 const userInfo = this.globalData.userInfo || { role: 'user' }; const tabList = getTabListByRole(userInfo.role); this.globalData.tabList = tabList; }, globalData: { userInfo: null, tabList: [] } });3.3 页面集成与选中态同步
这是实现中最容易出错的环节。我们需要在每一个需要显示tabBar的页面中,做以下几件事:
1. 页面WXML中引入组件:
<!-- 假设页面路径是 /pages/home/index.wxml --> <view class="page-container"> <!-- 你的页面主要内容 --> <view>这里是首页内容</view> </view> <!-- 在页面底部引入自定义tabBar --> <custom-tab-bar tabList="{{tabList}}" currentIndex="{{currentTabIndex}}" bind:tabchange="onTabChange" />2. 页面JS中配置和使用:
// pages/home/index.js const { getTabListByRole, getCurrentTabIndex } = require('../../utils/tabConfig'); const app = getApp(); Page({ data: { tabList: [], // 当前页面的tab配置 currentTabIndex: 0, // 当前选中的tab索引 }, onLoad(options) { // 方案A:从全局数据获取(适用于所有页面tab一致) // const globalTabList = app.globalData.tabList; // const currentPath = `/${this.route}`; // 获取当前页面路径 // const index = getCurrentTabIndex(currentPath, globalTabList); // this.setData({ tabList: globalTabList, currentTabIndex: index }); // 方案B:页面自己根据角色计算(更灵活,可做页面级定制) const userRole = app.globalData.userInfo?.role || 'user'; let tabList = getTabListByRole(userRole); // 可以在此处针对当前页面再做微调,例如首页不需要消息角标 // tabList = tabList.map(tab => ({...tab})); const currentPath = `/${this.route}`; const index = getCurrentTabIndex(currentPath, tabList); this.setData({ tabList, currentTabIndex: index }); }, // 监听tabBar切换事件 onTabChange(e) { const { index, path } = e.detail; // 更新当前页面的选中状态 this.setData({ currentTabIndex: index }); // 执行页面跳转(这里跳转逻辑也可以放在组件内,但放在页面层更便于统一管理跳转前的逻辑,如数据保存提示) this._handleTabNavigation(path, index); }, _handleTabNavigation(targetPath, targetIndex) { const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const currentPath = `/${currentPage.route}`; // 如果点击的就是当前页面对应的tab,不跳转(组件已处理) if (targetPath === currentPath) return; // 智能路由判断:目标页面是否已在页面栈中? const targetPageInStack = pages.find(page => `/${page.route}` === targetPath); if (targetPageInStack) { // 存在,则回退到该页面 const delta = pages.length - pages.indexOf(targetPageInStack) - 1; wx.navigateBack({ delta }); } else { // 不存在,则打开新页面 wx.navigateTo({ url: targetPath }); } }, // 重要:监听页面显示,当用户通过导航栏返回或安卓物理返回键返回时,同步tab选中态 onShow() { const currentPath = `/${this.route}`; const index = getCurrentTabIndex(currentPath, this.data.tabList); // 防止不必要的setData if (index !== this.data.currentTabIndex) { this.setData({ currentTabIndex: index }); } } });核心技巧与避坑指南:
onShow中同步选中态:这是保证体验连贯性的关键。当用户点击手机物理返回键或小程序左上角返回按钮时,页面栈变化了,但自定义tabBar组件并不知道。我们需要在页面的onShow生命周期里,根据当前页面路径重新计算并更新currentTabIndex。- 智能路由逻辑:
_handleTabNavigation方法实现了“如果目标页面已在栈中,则回退而非新建”。这能避免页面栈无限增长(例如用户反复点击同一个tab)。判断逻辑依赖于getCurrentPages()API,它能获取当前页面栈的所有实例。- 路径处理:注意
this.route获取的是如pages/home/index这样的路径,需要手动加上/前缀才能与pagePath匹配。
3.4 超过5个Tab的布局与交互处理
当tab数量超过5个时,直接等分排列会显得非常拥挤。这里提供两种主流解决方案:
方案一:可滑动Tab栏(适用于6-8个)通过CSS让tab容器可以横向滚动。
/* custom-tab-bar.wxss 修改 */ .custom-tab-bar { display: flex; flex-direction: row; /* 默认横向 */ overflow-x: auto; /* 允许横向滚动 */ white-space: nowrap; /* 防止子项换行 */ /* 移除 flex: 1 的等分,给子项固定或最小宽度 */ } .tab-item { flex: none; /* 不自动伸缩 */ min-width: 120rpx; /* 设置最小宽度 */ display: inline-flex; /* 或保持flex,但容器需调整 */ }同时,在JS中监听滚动,可以添加一个指示器来显示当前可见区域。
方案二:Tab栏结合“更多”下拉菜单(适用于更多Tab)这是更常见的方案。显示前4个或5个最重要的tab,最后一个tab固定为“更多”(一个省略号或菜单图标)。点击“更多”弹出一个覆盖层或下拉菜单,展示剩余的tab项。
WXML部分修改:
<view class="custom-tab-bar"> <!-- 常规显示的tab --> <block wx:for="{{visibleTabList}}" wx:key="pagePath"> <!-- ... tab项渲染 ... --> </block> <!-- “更多”按钮 --> <view wx:if="{{hiddenTabList.length > 0}}" class="tab-item more" bind:tap="onMoreTap"> <text class="iconfont icon-more"></text> <text class="tab-text">更多</text> </view> </view> <!-- 更多菜单 - 使用小程序原生或自定义模态框 --> <view wx:if="{{showMoreMenu}}" class="more-menu-overlay" catch:tap="hideMoreMenu"> <view class="more-menu-content"> <block wx:for="{{hiddenTabList}}" wx:key="pagePath"> <view class="menu-item">// 在收到消息数更新时 const newTabList = this.data.tabList.map(tab => { if (tab.pagePath === '/pages/message/index') { return { ...tab, badge: messageCount }; } return tab; }); this.setData({ tabList: newTabList }); // 如果使用全局状态,则更新全局状态并通知所有页面页面生命周期onTabItemTap: 原生tabBar页面特有的这个生命周期将不会被触发。如果业务逻辑依赖于此(例如点击tab刷新页面),我们需要在自定义组件的onTabItemTap事件中,手动触发当前页面实例的某个自定义方法(如前面代码注释中的currentPage.onTabItemTap && currentPage.onTabItemTap()),或者在页面的onShow中根据条件判断是否来自tab切换来执行刷新。
4.2 动画与交互体验优化
流畅的动画能极大提升质感。
- 选中态动画:可以为激活的tab项添加一个平滑的背景色变化或图标缩放动画。
.tab-item.active .tab-icon { transform: scale(1.1); transition: transform 0.2s ease; } - 点击反馈:使用
hover-class属性为tab项添加点击时的临时样式(如背景色变灰),模拟原生按压效果。<view class="tab-item {{currentIndex === index ? 'active' : ''}}" hover-class="tab-item-hover" ... >.tab-item-hover { background-color: rgba(0, 0, 0, 0.05); } - “更多”菜单动画:使用WXSS的
transition和transform实现菜单的淡入和滑入效果。.more-menu-content { transform: translateY(-10px); opacity: 0; transition: all 0.3s ease; } .more-menu-content.show { transform: translateY(0); opacity: 1; }
4.3 性能优化要点
- 减少不必要的setData:在
onShow中更新currentTabIndex时,先判断值是否真的变化了。组件的tabList如果来自全局且不常变化,可以使用observers监听,而不是每次onShow都setData整个列表。 - 图片图标优化:使用雪碧图(CSS Sprite)或字体图标来减少HTTP请求。对于图片,务必确保尺寸合适并经过压缩。
- 组件按需引入:如果“更多”菜单等复杂弹窗不是每个页面都需要,可以考虑将其做成独立组件,在需要时才引入和渲染,减少初始包体积和内存占用。
- 数据缓存:用户角色和对应的tab配置在单次会话内通常不变,可以缓存在
storage中,避免每次启动都重新计算。
5. 常见问题排查与实战技巧
5.1 问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 自定义tabBar不显示 | 1. 页面JSON未声明usingComponents。2. 组件路径引用错误。 3. 页面容器高度未设置,tabBar被内容遮挡。 | 1. 在页面json的usingComponents中添加组件。2. 检查WXML中组件标签的路径。 3. 确保页面容器有 padding-bottom或使用safe-area。 |
| 点击tab无反应/不跳转 | 1. 点击事件未绑定或绑定错误。 2. 跳转使用了 wx.switchTab。3. pagePath路径与app.json中pages配置不匹配。 | 1. 检查bind:tap绑定。2.全部改用 wx.navigateTo。3. 确保 pagePath以/开头,且页面已在pages中注册。 |
| 选中态高亮错误 | 1.currentIndex计算逻辑错误。2. 页面 onShow中未同步状态。3. 通过非tabBar方式(如分享卡片)进入页面。 | 1. 调试getCurrentTabIndex函数。2. 确保在 onShow中根据当前路由更新索引。3. 在 onLoad和onShow中都进行同步。 |
| iPhone底部tabBar被遮挡 | 未适配底部安全区域。 | 在组件样式中添加padding-bottom: env(safe-area-inset-bottom);。 |
| “更多”菜单点击穿透 | 菜单浮层未阻止底层页面滚动或点击。 | 使用catch:tap绑定菜单遮罩层关闭事件。菜单内容区域使用catch:tap阻止事件冒泡。 |
| 页面栈过深 | 反复点击同一个tab,每次都wx.navigateTo。 | 实现“智能路由”,判断目标页面是否在栈中,是则用navigateBack回退。 |
5.2 实战技巧与心得
- 统一路由管理:将所有的页面跳转逻辑(包括tab跳转和非tab跳转)封装到一个统一的
router工具函数中。在这个函数里集中处理智能路由、登录拦截、参数传递等逻辑,使页面代码更简洁。 - 角色权限的细粒度控制:我们的
tabConfig.js只控制了tab的可见性。实际上,还可以在ALL_TABS_META中为每个tab关联一个permission字段(如'view_dashboard')。在生成tabList时,不仅检查角色,还可以调用一个权限检查函数,实现更细粒度的控制。 - Tab配置云端化:对于需要频繁调整tab顺序、文案或图标的运营需求,可以将
ROLE_TAB_MAP甚至ALL_TABS_META放到后端数据库中。小程序启动时或用户登录后,从接口拉取最新的配置。这样无需发版即可更新导航栏。 - 自定义tabBar的“隐藏”:有些页面(如全屏的视频播放页、文章详情页)可能需要隐藏tabBar。我们可以在那个页面的
onLoad里,通过this.selectComponent('#yourTabBarId')获取组件实例,然后调用组件的一个方法(如.hide())来隐藏它,并在onUnload或onHide中显示。组件内部通过wx:if或hidden控制显示。 - 首次加载白屏优化:如果tabList依赖网络请求,可能导致组件初始化时数据为空。可以在组件内设置一个默认的加载态或占位符,等数据准备好后再渲染正式内容。
实现一套健壮的自定义动态tabBar,初期投入的工作量确实比配置原生tabBar大得多。但一旦搭建完成,它带来的灵活性是巨大的。无论是应对多角色、超数量tab,还是未来各种千奇百怪的运营需求,这套架构都能从容应对。