ARTICLE DETAIL

资讯详情

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

uniapp自定义底部导航栏实现与切换闪烁问题终极解决方案

uniapp自定义底部导航栏实现与切换闪烁问题终极解决方案 做uniapp开发这几年凡是要做底部导航的项目基本都逃不开“自定义tabbar”这个坎。产品经理一句话中间来个凸起按钮最好带个旋转动画另一个tab要动态红点角标某些页面还得把tabbar藏起来只留一个悬浮球。原生tabbar翻来覆去就那么几个配置项想支持这些基本没戏于是大家都走向了同一条路——自定义底部导航栏。自定义本身并不难真正让人抓狂的是后面切换选项卡的时候页面“闪”一下白屏、图片重新加载、内容跳动体验直接被打回原型。这篇文章就是把两件事一起讲透怎么优雅地实现自定义底部导航栏以及怎么从根上解决切换闪烁问题。不管你是刚接触uniapp的新手还是已经被这个问题磨了两三天的老开发这套方案拿过去都能直接落地。1. 为什么需要自定义底部导航栏——方案选型与核心取舍1.1 原生tabbar的四个明显短板先说结论原生tabbar不是不能用而是一旦产品需求超过它的能力边界硬用就会变成“拿胶带补船”。我归纳了四个最常见的触发点。第一特殊形态按钮。中间凸起、半悬浮、圆形加大号按钮这类设计在电商、直播、工具类App里出现频率极高。原生tabbar的每个item高度和样式都是引擎固定的凸起效果只能靠覆盖样式很多端上还覆盖不完整尤其是小程序和App端效果经常和设计稿差很远。第二动态角标与红点。原生tabbar虽然提供了uni.setTabBarBadge这类API做角标但样式十分受限——只能显示数字或红点不能自定义背景色、动效更不支持放一张自定义图片。真正的业务场景里角标常常要跟着消息、购物车数量、优惠券状态实时变化原生方案很难做精致。第三样式定制自由度过低。选中态文字加粗、背景渐变、图标切换动画、tabbar整体透明悬浮……这些在原生配置里要不做不了要不各端实现不一致。H5上改起来容易小程序和App端就非常折腾。第四tabbar的显隐控制不灵活。典型场景是首页进入详情页、再进入某个需要全屏展示的页面这时候tabbar应该隐藏返回后再恢复。原生tabbar的显隐控制APIuni.hideTabBar在个别端上会有白条残留或闪烁用户体验不完美。当然还有一种情况更隐蔽项目要同时发布小程序、App、H5三端原生tabbar在不同端的默认表现差异明显为了统一视觉和交互自定义成了唯一出路。1.2 两条技术路线动手之前先想清楚自定义底部导航栏目前主流有两条路线很多教程只讲其中一条导致读者做完才发现不适合自己的业务场景。这里先做一次对比。路线A保留原生tabBar配置设置custom: true自己写一个tabbar组件页面跳转仍然使用uni.switchTab。这种方案下每个tab仍然是一个独立页面有自己的onLoad、onShow、onHide生命周期。优点很明显页面职责清晰、业务代码隔离干净、页面栈由框架管理。缺点则是页面切换时底层仍然发生页面级的show/hide白屏闪烁的风险天然存在后面要花不少精力优化。路线B完全放弃原生tabBar新建一个“容器页面”把几个tab对应的页面内容封装成子组件在容器页里用v-show或v-if切换。这种方案相当于把页面切换变成了组件切换DOM结构常驻内存切换时不会销毁重建闪烁问题从机制上被绕开了。但它也有代价原本写在页面生命周期里的逻辑要迁移到组件的created、mounted以及自定义事件里改动量不小。我见过太多人一上来就选路线A做完被闪烁问题折磨几天后又想着改成路线B结果代码改造成本已经很高。所以强烈建议做之前先对照自己项目的实际情况选型。下面这个表格是我反复用过很多次的判断依据。对比维度路线Acustom:true switchTab路线B容器页 v-show切换页面生命周期正常触发无需改造需要手动迁移用自定义事件模拟页面状态保留小程序端较好App端有时会重绘组件常驻内存状态天然保留切换闪烁页面级切换存在白屏风险只有DOM显隐切换基本不闪业务代码结构每个tab一个页面隔离清晰所有tab集中在容器页靠组件拆分数据共享需要借助store或全局变量同在一个父组件下传参更直接改造工作量较低写组件即可较高需要迁移生命周期逻辑适合场景tab功能独立页面复杂维护优先对切换流畅度要求高功能可组件化就我个人经验已经跑起来的存量业务选路线A配合优化手段是止血最快的方式而新项目如果团队预算允许我会直接上路线B虽然初期多花一两天改造但换来的是长期稳定的切换体验这个账很划算。1.3 页面状态保留——一个被很多人忽略的体验暗坑除了闪烁本身自定义tabbar还会带出另一个问题切换tab之后再切回来页面状态丢了。具体表现有几种列表滚动位置回到顶部、表单填了一半的内容被清空、已经加载的数据又重新loading一遍。这些现象虽然不是“闪烁”的字面意思但叠加在一起用户感知就是“这页面好卡、好不连贯”。造成状态丢失的原因路线A里是页面实例在某些端上被销毁重建或者onShow里重新拉了数据路线B里则是很多人用了v-if切换导致组件被销毁状态自然全丢。所以后文解决闪烁问题时我会把状态保留一起讲这两件事必须一起解不能分开处理。2. 自定义底部导航栏实操——从配置到组件封装2.1 pages.json配置custom: true 不是万能钥匙很多人以为自定义tabbar就是把pages.json里的tabBar配置删了完全自己写。这样做在小程序端有个坑如果页面路径不在tabBar.list里uni.switchTab就无法跳转到这个页面点击tabbar就会失败。所以正确的做法是保留tabBar配置同时打开自定义开关。以四个tab为例pages.json长这样{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页 } }, { path: pages/category/category, style: { navigationBarTitleText: 分类 } }, { path: pages/cart/cart, style: { navigationBarTitleText: 购物车 } }, { path: pages/mine/mine, style: { navigationBarTitleText: 我的 } } ], tabBar: { custom: true, color: #999999, selectedColor: #fa2c19, backgroundColor: #ffffff, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/category/category, text: 分类 }, { pagePath: pages/cart/cart, text: 购物车 }, { pagePath: pages/mine/mine, text: 我的 } ] } }关键点有两个一是custom必须设为true这样框架才会允许你自己渲染tabbar二是list绝对不要删它决定了哪些页面属于tab页面uni.switchTab依赖这份配置来识别可跳转的页面。另外color、selectedColor、backgroundColor这些字段在自定义模式下其实不会再影响界面但保留着也无妨某些工具或云测平台可能会读取这些字段做校验。这里顺带提一个相关但容易踩的配置点如果项目里用了uni.hideTabBar来控制tabbar显隐在自定义模式下这个API是无效的因为真正的tabbar已经是你自己的组件了你要隐藏它直接控制组件v-if或v-show即可。很多人不知道这一点在自定义模式下还到处找hideTabBar失效的原因找了半天发现方向就错了。2.2 BottomNav组件封装——从结构到样式一次到位自定义tabbar的载体是一个通用的BottomNav.vue组件。这个组件要做到“一次封装到处复用”我建议把数据源抽成list把当前选中项抽成current页面只负责传参组件只负责渲染和通知事件。下面是组件核心代码基于vue2写法vue3语法上略有差异但思路完全一致template view classbottom-nav view v-for(item, index) in list :keyitem.pagePath classnav-item :class{ active: current index, nav-item--center: item.center } clickhandleSwitch(index, item) view classicon-wrap image classicon :srccurrent index ? item.selectedIconPath : item.iconPath modeaspectFit / view v-ifitem.badge item.badge 0 classbadge {{ item.badge 99 ? 99 : item.badge }} /view /view text classtext{{ item.text }}/text /view /view /template script export default { name: BottomNav, props: { current: { type: Number, default: 0 }, list: { type: Array, default: () [] } }, methods: { handleSwitch(index, item) { if (index this.current) return // 通知父级方便做埋点、统计等操作 this.$emit(change, { index, item }) // 路由仍然交给框架的switchTab处理 uni.switchTab({ url: item.pagePath }) } } } /script style scoped .bottom-nav { position: fixed; left: 0; right: 0; bottom: 0; height: 100rpx; display: flex; background: #ffffff; box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06); padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); z-index: 100; } .nav-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; } .icon-wrap { position: relative; width: 48rpx; height: 48rpx; } .icon { width: 48rpx; height: 48rpx; } .badge { position: absolute; top: -8rpx; right: -16rpx; min-width: 32rpx; height: 32rpx; padding: 0 8rpx; background: #fa2c19; color: #ffffff; font-size: 20rpx; line-height: 32rpx; text-align: center; border-radius: 16rpx; } .text { margin-top: 4rpx; font-size: 22rpx; color: #999999; } .nav-item.active .text { color: #fa2c19; } /style这个组件有几个细节是实战中总结出来的。首先是安全区适配iPhone X以后机型底部有home indicator不处理的话tabbar会顶到最下面和home indicator重叠。上面代码里的env(safe-area-inset-bottom)是标准写法建议每个项目都加上。其次是角标的定位一定要把角标放在icon-wrap这个相对定位容器内不要直接从nav-item上绝对定位否则不同机型上位置偏差很大。最后是事件通知机制click里同时做两件事通过$emit(change)通知父级业务层再执行uni.switchTab跳转。这样跳转之前父级有机会记录埋点、更新状态行为可追溯。2.3 页面接入与选中状态同步有了BottomNav组件页面里的接入就非常简单了。以首页为例template view classpage view classpage-content !-- 首页业务内容 -- /view BottomNav :current0 :listtabList changehandleTabChange / /view /template script import BottomNav from /components/BottomNav.vue export default { components: { BottomNav }, data() { return { tabList: [ { pagePath: /pages/index/index, text: 首页, iconPath: /static/tab-home.png, selectedIconPath: /static/tab-home-active.png }, // ...其他tab配置 ] } }, methods: { handleTabChange({ index, item }) { // 这里可以做tab切换埋点 console.log(切换到, index, item) } } } /script其他几个tab页面也按同样方式接入把current改成对应下标即可。不过这里有个体验隐患每个页面各自维护current和tabList容易重复定义。更好的做法是把tabList抽到一个公共配置文件中页面里import进来current则根据当前页面路径计算或者在store里维护一份“当前选中tab下标”。我的习惯是后者因为页面切换之后store里的状态可以保证全局唯一tabbar的高亮不会因为某次刷新出现错乱。还有一个容易被忽略的细节uni.switchTab的url必须以/开头写全路径否则在小程序端会直接报错找不到页面。这个错误非常低级但确实有同事因为漏了一个斜杠排查了半天。2.4 角标、红点与中间凸起按钮的实现细节先说角标。角标数据天然是全局的购物车数量、未读消息数都来自不同页面所以不要在BottomNav里维护角标数据而是让list里的badge字段从store里读取。比如vuex中维护一个badgeMap然后在页面计算属性里把badgeMap合并进tabList再传给BottomNav。这样任何业务页面更新了badgeMaptabbar角标会响应式更新无需额外通知机制。再说中间凸起按钮。这种设计在电商和同城服务类项目里非常常见实现思路也不复杂在list配置中给某个item加上center: true标记然后在样式里对这个item做特殊处理。核心代码如下.nav-item--center { margin-top: -40rpx; } .nav-item--center .icon-wrap { width: 96rpx; height: 96rpx; border-radius: 50%; background: #fa2c19; display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 20rpx rgba(250, 44, 25, 0.3); } .nav-item--center .icon { width: 48rpx; height: 48rpx; }思路是利用负margin-top让中间按钮向上凸出然后给图标容器加圆形背景和阴影。需要注意凸出部分容易超出tabbar高度如果tabbar背景是不透明的纯色超出部分会被遮挡这时需要给.bottom-nav加上overflow: visible并且把凸出的容器层级抬高z-index。如果tabbar本身要盖在内容之上建议凸出的圆形按钮背景使用与tabbar背景不同的强调色视觉上更有层次。3. 切换选项卡页面闪烁的根因拆解3.1 先界定“闪烁”——用户看到的究竟是什么解决闪烁问题之前一定要先界定清楚“闪烁”指什么。我排查过不少项目发现大家口中的“闪烁”其实包含好几种不同的现象根因各不相同对应方案也完全不同。第一种是白屏闪烁切换tab的瞬间页面先白一下然后再渲染出内容。这种情况在App端最常见本质是新页面还没有完成首帧渲染之前webview把默认背景色暴露出来了。第二种是内容跳变切换tab后页面先出现一小段无样式文本或布局错乱的内容然后再“跳”成正常样式。这经常是因为CSS加载晚于页面结构渲染或者是图片还没加载完成撑不起高度。第三种是图片闪动切回某个tab时图片先显示成空白或灰色占位再突然出现。这是因为图片资源被重新解码加载或者走了lazy-load机制滚动到可视区域才开始加载。第四种是loading闪烁切回tab后页面重新进入loading状态先展示loading组件然后才展示数据。这其实是代码逻辑问题但用户感知和闪烁完全一样。所以当你觉得“我的tabbar切换闪烁”时先解剖一下具体是哪种现象。这步省不得因为它决定了你后面是改页面结构、改数据逻辑还是改资源加载策略。3.2 路线A的根因页面实例创建与onShow数据抢占路线A使用uni.switchTab切换独立页面底层是在多个页面实例之间切换展示。切换过程中框架要做的事情很多隐藏当前页面、通知onHide、显示目标页面、触发目标页面的onLoad或onShow。这一整套动作如果目标页面初始化成本高白屏就会被放大。初始化成本高在哪里最典型的是onLoad和onShow里同步执行了大量数据处理。比如从uni.getStorageSync读取一个大对象、在onShow里直接发请求并等待数据返回再渲染、或者在页面onLoad时同步创建多个复杂的图表实例。这些操作都会阻塞首帧渲染用户看到的就是白屏或者loading。还有一个原因是页面里的图片资源没有做预加载。切换到目标页面后页面的图片才开始从网络或磁盘读取小图还好大图、轮播图、背景图就会造成明显的视觉“空窗期”。这个问题在低端安卓机上尤其明显图片解码速度慢闪烁感被进一步放大。3.3 路线B的根因v-if销毁重建与资源重复解码路线B的闪烁和路线A完全不同。很多人写容器页时习惯用v-if控制组件显示隐藏以为这样“更节省资源”。这个想法本身没错但在tab切换场景下它带来的问题比省下的资源更严重。使用v-if时每次切换都意味着上一个tab的组件被销毁下一个tab的组件被重新创建。组件内部的image会重新发起加载复杂view结构的重建也需要时间。如果某个tab页面里有一个大列表从创建到渲染出第一屏可能要几百毫秒这段时间用户看到的就是一块空白区域和“白屏”的体感没有区别。更要命的是v-if会丢失所有内部状态。列表滚动位置、表单输入内容、组件内部缓存的请求结果全部归零。切回来又要重新初始化整个过程叠加下来用户感受到的就是“一直在加载、一直在闪”。3.4 图片与大尺寸资源对闪烁的放大作用图片是闪烁问题中最容易被单独拎出来说的原因因为它对视觉的影响最大。我自己排查过的一个项目tab切换闪烁特别严重后来定位发现是每个tab页面的顶部都有一张大尺寸背景图。每次切回这个tab背景图都要重新加载和解码白屏期间这张图一直是空白等图出来页面才“完整”。图片问题的另一个隐藏点在于uniapp的image组件在不同的端上缓存策略不一样。小程序端图片有本地缓存但第一次展示时有解码耗时App端H5页面里图片走的是WebView缓存有时候缓存未命中就要重新下载。H5端更直接页面一旦重新渲染图片就要重新走一遍资源加载流程。所以图片不是唯一原因但它是让闪烁“肉眼可见”的放大器。解决闪烁问题时图片优化必须当作重点项目来做。4. 实战一步步解决页面闪烁问题4.1 路线A优化——骨架屏、缓存优先、静默刷新如果你的项目已经用了路线A又不想大范围重构下面这套组合拳可以先止血。第一步加骨架屏。在页面渲染真正数据之前先用纯view和css搭出页面的基本框架比如标题条、列表占位块、图片占位块。骨架屏不依赖任何请求数据首帧就能渲染用户看到的是“页面已经出来了内容正在加载”观感上比白屏好很多。骨架屏可以做成一个通用组件传入区块类型即可复用。第二步做数据缓存优先。页面onShow时不要急着发请求先读本地缓存渲染旧数据同时后台静默请求新数据请求成功后用新数据覆盖旧数据。这样用户切回tab时看到的永远是有内容的状态而不是loading。第三步是避免在onLoad里执行高开销操作。凡是能延后的操作一律延后比如统计上报、非关键配置读取、第三方SDK初始化全部放到页面渲染完成之后。第四步是给页面的首屏图片做预加载。在App启动后或进入第一个tab时预先把其他几个tab首屏的关键图片下载到本地。这个方案后面会详细讲怎么做。这套优化做完路线A的闪烁虽然不能100%消失但基本能从“频繁闪”降到“极少闪”大多数业务场景已经够用了。4.2 路线B优化——v-show替代v-if懒渲染保底如果你用的是容器页方案核心优化就一句话把v-if换成v-show。只要组件没有被销毁DOM结构就一直在图片不会重新解码状态不会丢失闪烁自然就没了。但这里有一个内存和渲染性能的取舍。如果把四个tab的组件全部用v-show常驻那么容器页初始化时四个组件的created和mounted会同时触发如果每个tab内部都有大量数据请求和复杂渲染首屏加载会变慢甚至出现卡顿。解决办法是“懒渲染”加“常驻显示”的组合第一次切换到某个tab时才初始化这个组件一旦初始化过之后就用v-show让它常驻。实现方式不复杂核心代码如下template view classtabbar-container HomePage v-ifhomeMounted v-showcurrent 0 reftabComps[0] / CategoryPage v-ifcategoryMounted v-showcurrent 1 reftabComps[1] / !-- 其他tab同理 -- BottomNav :currentcurrent :listtabList changeonTabChange / /view /template script export default { data() { return { current: 0, homeMounted: true, categoryMounted: false, // otherMounted同理 } }, methods: { onTabChange({ index }) { const prevIndex this.current this.current index // 首次切换的tab标记为已挂载 if (index 1 !this.categoryMounted) { this.categoryMounted true } // 通知子组件模拟onHide和onShow const prevComp this.$refs[tabComps][prevIndex] const nextComp this.$refs[tabComps][index] this.$nextTick(() { prevComp prevComp.onTabHide prevComp.onTabHide() nextComp nextComp.onTabShow nextComp.onTabShow() }) } } } /script这段代码里有三个关键点一是v-if只负责“是否初始化”初始化完成后就不再参与切换逻辑二是用v-show控制显示隐藏保证DOM和组件实例常驻三是通过onTabShow和onTabHide这两个自定义方法把原本页面级onShow/onHide的逻辑迁移到组件里。这样既保留了页面生命周期的语义又绕开了页面切换的闪烁问题。当然懒渲染也不是绝对的。如果四个tab都非常轻量直接全量v-show也可以如果某个tab特别重还可以写成v-if加内部缓存状态的方式但那样又回到了状态恢复的问题不如懒渲染省心。4.3 图片预加载与缓存实践图片预加载的通用做法是提前把关键图片“摸”一遍让系统缓存住。H5端可以直接用Image对象预加载小程序和App端用uni.getImageInfo更通用。// utils/preload.js export function preloadImages(urls) { urls.forEach((url) { // H5端 // const img new Image() // img.src url // 小程序端和App端 uni.getImageInfo({ src: url, complete: () {} }) }) }调用时机建议放在App启动后的空闲期或者在容器页onLoad之后延迟几百毫秒执行。预加载的图片范围只覆盖每个tab首屏的关键图不要把所有图片都扔进去否则预加载本身就可能拖慢页面。另外uniapp的image组件有一个lazy-load属性可以开启懒加载。但在tab切换场景下这个属性要慎用。如果图片位于首屏可视区域懒加载反而可能导致切换后图片延后加载造成“闪一下”。我的建议是首屏图关闭懒加载列表滚动区域的图可以开。还有一个实操细节图片资源尽量压缩到合理尺寸不要直接放设计稿原图。一张2MB的背景图和一张200KB的背景图在低端安卓上的解码速度差好几倍闪烁感也完全不是一个级别。现在各种在线压缩工具都很成熟这一步值得做。4.4 终极方案——容器页加v-show的组合拳怎么打上面几节的优化本质上都是“缓解”或“规避”而“终极方案”是从架构上绕开闪烁。这个方案我在1.2节的路线B里已经做了铺垫这里把完整的打法和踩过的坑都讲透。容器页加v-show的组合核心思路是整个tab切换过程中不涉及任何页面级的生命周期切换只有一个页面四个子组件切来切去都只是display属性的变化。这样一来页面不会重新创建WebView不会重新初始化图片不会重新解码闪烁从机制上被消灭。但要把这个方案落地必须解决两个问题。第一个问题是生命周期迁移。原本写在页面onLoad里的代码迁移到子组件的created或mounted里。两者执行时机不同页面onLoad在页面初始化时执行而子组件mounted在组件挂载到DOM后执行大部分场景下可以等价替换但如果代码里依赖uni.getSystemInfoSync等API要注意子组件mounted阶段这些API也是可用的问题不大。原本写在onShow里的代码迁移到容器页在切换tab时调用的onTabShow方法里。这里有个常见的坑很多人直接把onShow改个名就放到组件里但没有考虑首次进入的情况。首次进入时第一个tab应该也要执行onTabShow逻辑所以容器页的onLoad或mounted阶段要手动调用一次第一个tab的onTabShow。第二个问题是数据共享。以前多个tab页面之间共享数据要么走store要么用事件总线。在容器页方案里所有tab组件都是容器页的子组件直接用ref调用子组件方法、用props传参、用$emit回传事件链路更短、更直观。我建议把跨tab共享的数据尽量提升到容器页这一层管理或者继续使用store两种方式都比页面间通信省心。这个方案改完切换体验可以用“丝滑”来形容。我自己改造过的一个项目从路线A迁到容器页方案后tab切换再也没有出现过白屏列表滚动位置、筛选条件、表单内容全部原样保留整个应用的高级感直接提升一个档次。4.5 切换时序控制与滚动位置保留不管是路线A还是路线B切换时序都值得专门控制一下。核心原则是先保证界面不空再让数据更新。具体的做法是区分“首次进入”和“再次进入”。首次进入时页面没有旧数据该显示loading就显示loading该显示骨架屏就显示骨架屏再次进入时页面已经有旧数据了直接显示旧数据同时后台静默请求新数据请求成功后替换。判断逻辑可以用一个hasLoaded变量onTabShow() { if (!this.hasLoaded) { this.hasLoaded true this.fetchData(true) // 带loading } else { this.fetchData(false) // 静默刷新 } }这样的好处是用户切回tab时第一眼看到的是之前已经加载好的内容而不是重新loading无论数据新不新视觉上都不会闪。滚动位置保留方面路线B天然没问题因为组件没有销毁滚动位置自然保留。路线A则要看页面实例是否被系统销毁不同端行为不同。稳妥的做法是手动记录和恢复// 页面onHide时记录 onHide() { const query uni.createSelectorQuery() query.select(.page-body).boundingClientRect() query.selectViewport().scrollOffset() query.exec((res) { this.savedScrollTop res[1].scrollTop }) } // 页面onShow时恢复 onShow() { if (this.savedScrollTop) { uni.pageScrollTo({ scrollTop: this.savedScrollTop, duration: 0 }) } }这类代码虽然繁琐但对用户体验的提升非常直接。有些细节不做用户也说不出来但做了之后整个应用会显得特别“顺”。5. 常见问题与排查技巧实录5.1 常见问题速查表把过去几年被问得最多的几个问题整理成一张表每个问题对应一个最可能的排查方向。表中列出的排查方向是我在多个项目中验证过的按照顺序查基本能定位问题。问题现象最可能的根因排查方向与解决建议切换tab白屏目标页面首帧渲染过慢检查onLoad/onShow里是否有高开销同步操作加骨架屏切换tab图片闪一下图片重新解码或未走缓存预加载关键图片首屏图关闭懒加载tabbar点击无反应switchTab路径与pages.json不一致检查tabBar.list中的pagePath是否以/开头且完全一致角标不更新角标数据没有响应式绑定确认badge数据是否来自store的响应式字段切回tab又loadingonShow每次强制请求并且显示loading区分首次进入和非首次进入二次进入静默刷新iPhone底部被遮挡未适配安全区给tabbar加safe-area-inset-bottom适配H5端刷新后tabbar高亮错误当前选中态只存在内存中刷新后根据当前路由路径计算current值App端切tab页面闪黑WebView初始化慢尝试切换路由为容器页v-show方案5.2 小程序、App、H5三端的差异与适配uniapp的跨端能力很强但自定义tabbar在不同端的底层行为差异相当大这也是很多人“在H5上测试一切正常发布到小程序或App就出问题”的根本原因。小程序端的机制是所有tab页面默认常驻页面栈顶部。使用uni.switchTab切换时页面实例不会销毁onShow会重新触发。这对路线A是个好消息页面状态保留相对容易。但要注意小程序的页面栈有深度限制如果大量使用navigateTo跳转页面栈过深时旧的页面会被销毁tabbar自定义组件的状态也会跟着丢。这种情况建议用redirectTo或reLaunch来管理非tab页面的跳转。App端的机制更复杂。uni-app在App端默认使用WebView渲染页面每个tab页面是独立的WebView实例。切换tab时系统在多个WebView之间做显示隐藏如果目标WebView还没有加载完成就会露出白底或黑底。这就是为什么App端白屏闪烁尤其严重。解决思路要么是优化页面初始化性能要么直接放弃多页方案改用容器页v-show。另外App端如果用了nvue页面切换动画和性能表现会更好但nvue在组件兼容性上限制更多需要权衡。H5端的机制最接近传统Web SPA。使用uni.switchTab时H5端实际是路由跳转每次跳转都是一次页面重新渲染。如果项目部署在低性能设备上切换时可能出现明显白屏。路线B的容器页方案在H5端几乎零成本实现因为组件切换本来就是前端最擅长的操作。如果你的目标用户大量使用H5我会毫不犹豫建议容器页方案。5.3 几个容易被忽略的坑最后分享几个不大容易想到、但踩过一次就忘不掉的坑。第一个坑是自定义tabbar组件被页面重新创建。路线A里如果tabbar组件不是放在固定页面中而是被某些条件渲染包裹比如页面有个v-if控制整体显示切换回来时组件会重新初始化视觉上就会出现tabbar本身闪一下的现象。排查方法很简单在BottomNav的created里打一个日志如果切tab频繁打印说明组件被反复创建了。解决办法是确保tabbar挂载在页面根节点且不被条件渲染控制。第二个坑是iconfont图标字体在切换后不显示。如果tabbar使用字体图标而不是图片在某些端上切回页面时会先显示一个方块或空白过一会儿字体文件才生效。这是因为字体文件加载有延迟而且字体文件有时候不会走普通图片的缓存路径。解决方法是把iconfont转成图片或者确保字体文件被合理预加载。第三个坑是切换动画的过度设计。有人想通过加动画来掩盖闪烁结果动画和切换机制打架反而更卡。我个人经验tab切换尽量不要做复杂的页面级转场动画简单的透明度过渡就够了。复杂的动画只会增加渲染负担在低端机上适得其反。第四个坑是关于manifest.json配置。如果你在App端使用了自定义tabbar记得在manifest.json的app-plus节点中确认animationType等相关配置。某些配置会导致页面切换使用不同的动画方式影响闪烁感的强弱。例如pop-in这类动画在App端可能加重白屏感改为none或slide-in-bottom效果更柔和。具体参数可以根据真机效果调这个没有统一标准但值得花几分钟试一下。我自己经历了从“写组件解决需求”到“写架构解决体验”的转变。早期接到自定义tabbar需求上去就写组件写完组件就遇到闪烁然后加骨架屏、加预加载、调时序折腾来折腾去。后来想明白了一个道理——自定义tabbar只是表象真正要解决的是多页面切换带来的渲染成本问题。如果你还在项目初期强烈建议直接上容器页v-show的组合从根上绕开这一整类问题。底部导航这种东西稳定、顺滑、状态不丢比任何花哨的效果都重要。希望这篇文章里的方案和踩坑记录能让你在这个高频需求上少走一段弯路。
返回列表