尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

uniapp自定义底部导航栏:页面切换闪烁的根因分析与实战解决

uniapp自定义底部导航栏:页面切换闪烁的根因分析与实战解决 最近在做 uniapp 项目时被底部导航栏折腾得够呛。项目初期图省事直接在 pages.json 里配了原生 tabBar结果产品经理一句“这个选中状态要换个渐变图标”“中间那个按钮要凸起来”原生 tabBar 直接原地宣布报废。好那就自己造轮子呗。结果轮子造完新的问题又冒出来了点击切换选项卡的时候页面内容总是“闪”一下特别是列表页和带图片的页面闪得那叫一个明显用户都以为页面崩溃了。后来翻了社区、查了文档、又在自己项目里反复定位测试总算把这个问题从原理到方案摸清了。这里我把自定义底部导航栏的整体思路、切换页面闪烁的原因分析、以及我最终采用的解决方案完整记录下来。不是纯翻译文档是我自己踩坑踩出来的实操经验希望能帮你少走点弯路。1. 项目现状分析为什么原生 tabBar 满足不了需求1.1 原生 tabBar 的局限性先理清一个前提uniapp 项目里原生 tabBar 是挂在 pages.json 里的。它属于小程序或 App 原生层面渲染的组件地位超然普通组件根本盖不住它。原生 tabBar 最大的优势是稳定、流畅、切换无闪烁但它的问题也很直接图标和文字只能静态配置不能动态修改任何样式比如“角标数量变了图标换成动态的”做不了。选中态图片和文字颜色是写死的几套属性想做“渐变选中图标”这种设计基本没戏。中间的“凸起按钮”或“特殊形状”原生 tabBar 根本支持不了除非你把它当作页面的悬浮组件处理但层级和交互又会有新的问题。无法监听 tabBar 的点击事件不能做“点击当前 tab 回到顶部”“双击刷新”这类交互。这些限制在普通业务里还勉强能忍一旦产品经理提出“自定义一点”的要求原生 tabBar 就会成为整个项目里最尴尬的组件——你在页面里再怎么写它都纹丝不动。1.2 常规自定义方案的核心痛点既然原生的不行大家自然而然会想到两条路每条 tab 页面的 v-if 控制写一个自定义 tabBar 组件页面切换时通过 v-if 切换页面内容。cover-view 方案在 App 端使用 cover-view 实现可覆盖原生组件的 tabBar。第一种方案是最常见的。做法是在 index 页面里引入两个子页面组件用 v-if / v-show 控制切换把底部导航栏也做成一个组件通过 defineProps 或全局状态控制当前选中的 tab。这套方案功能上没问题但还是有两个关键痛点页面数据无法保持。因为 v-if 切换页面销毁重建列表滚动位置、表单输入内容全丢了。v-show 虽然能保留 DOM 和数据但所有页面同时渲染生命周期变得非常奇怪初始性能损耗大。切换“闪烁”。v-if 切换子组件瞬间底部导航栏和页面内容存在“新旧组件同时存在”的窗口期视觉上就会看到白屏或闪一下。而且这还没算上“页面栈”这个核心问题。uniapp 本质上是多页面框架pages.json 里注册的每个页面都有自己的页面栈。你在 index 页面里用 v-if 切换内容本质上根本没有切换页面只是切换子组件这跟原生 tabBar 那种“每个 tab 就是独立页面栈”的体验差距很大。cover-view 方案是另一个老牌做法。cover-view 是专门设计用来覆盖原生组件的可以盖在 video、map 这些原生组件上面。用它做 tabBar 的确能解决“控制层级”的问题但如果做复杂交互比如点击弹出二级菜单、动画、圆角、阴影这些样式cover-view 的限制也多开发体验并不好。综合对比下来我最终的方案是保留页面栈结构用 uni.switchTab 控制真实页面切换底部导航栏做成全局组件并在每个 tab 页面上以组件形式引入配合页面生命周期来统一管理选中态。这个方案是 uniapp 社区里公认比较成熟的自定义 tabBar 方案也是解决闪烁问题的基础。2. 自定义底部导航栏的整体设计与实现2.1 从原生 tabBar 迁移到自定义 tabBar 的落地思路确认要自定义后第一件事就是改 pages.json把原生 tabBar 配置删掉。这一步千万别懒不然会出现“原生 tabBar 还在自定义 tabBar 又套了一层”的情况底部被双份导航遮挡页面高度也会算错而且原生 tabBar 还遮挡不到上面的自定义 tabBar层级问题会变得极其复杂。pages.json 里保留 tab 页面注册即可像下面这样{ 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 配置后页面内容区域会自动扩展到底部所以自定义 tabBar 组件必须用固定定位固定在屏幕底部。这里有一个非常重要的细节每个 tab 页面都必须预留出底部 tabBar 的高度区域。我一般用 CSS 变量或者公共样式类来处理。/* App.vue 或公共样式文件 */ page { --tabbar-height: 50px; } .tabbar-page { padding-bottom: var(--tabbar-height); }每个 tab 页面的根节点加上 tabbar-page 类这样页面内容就不会被固定定位的 tabBar 遮挡。2.2 自定义 tabBar 组件的基本实现自定义 tabBar 组件的核心是两部分数据配置和点击切换。先说数据配置我习惯把 tabBar 的配置单独抽到一个 JS 文件里这样改起来方便不用在组件里翻代码。// config/tabbar.js export default [ { path: /pages/index/index, text: 首页, icon: /static/tabbar/home.png, activeIcon: /static/tabbar/home-active.png }, { path: /pages/category/category, text: 分类, icon: /static/tabbar/category.png, activeIcon: /static/tabbar/category-active.png }, { path: /pages/cart/cart, text: 购物车, icon: /static/tabbar/cart.png, activeIcon: /static/tabbar/cart-active.png }, { path: /pages/mine/mine, text: 我的, icon: /static/tabbar/mine.png, activeIcon: /static/tabbar/mine-active.png } ];组件本身不复杂关键是选中状态的判断。这里不能简单地在组件内部用 current 值因为每个页面都是一个独立的页面实例组件在各自页面里渲染需要知道“当前是哪个页面”才能高亮对应的 tab。我在组件里通过 getCurrentPages() 获取当前页面路由然后和配置里的 path 做匹配。template view classtabbar view classtabbar-item v-for(item, index) in tabbarList :keyindex clickswitchTab(item) image classtabbar-item-icon :srccurrentPath item.path ? item.activeIcon : item.icon / text classtabbar-item-text :class{ tabbar-item-text_active: currentPath item.path } {{ item.text }} /text /view /view /template script import tabbarList from /config/tabbar.js; export default { data() { return { tabbarList: tabbarList, currentPath: }; }, created() { this.updateCurrentPath(); }, onShow() { // 页面每次显示时重新获取路径确保切换回来时高亮正确 this.updateCurrentPath(); }, methods: { updateCurrentPath() { const pages getCurrentPages(); const currentPage pages[pages.length - 1]; this.currentPath /${currentPage.route}; }, switchTab(item) { if (this.currentPath item.path) { return; } uni.switchTab({ url: item.path }); } } }; /script这里有个易错点getCurrentPages() 返回的页面对象里route 属性是不带斜杠的比如 “pages/index/index”而 tabBar 配置里我写的是 “/pages/index/index”所以要手动拼上斜杠进行匹配。另一个易错点是 onShow 生命周期。组件挂载在页面上页面从其他 tab 切回来时会触发页面 onShow但组件自身不一定触发 created/mounted所以要监听页面的 onShow 并重新获取路径。如果组件里配置了 onShow需要在组件的 options 里开启多页面刺探或者更简单的方式是把 currentPath 的逻辑放到父页面里通过 props 传给组件。我实践下来用 mixin 的方式更简单写一个 mixin在每个 tab 页面的 onShow 里计算当前路径赋值给 data然后在模板里给 tabBar 组件传 activePath prop。// mixins/tabbar.js export default { data() { return { activeTabPath: }; }, onShow() { const pages getCurrentPages(); const currentPage pages[pages.length - 1]; this.activeTabPath /${currentPage.route}; } };父页面里这样使用template view classtabbar-page !-- 页面内容 -- custom-tabbar :active-pathactiveTabPath/custom-tabbar /view /template script import customTabbar from /components/custom-tabbar.vue; import tabbarMixin from /mixins/tabbar.js; export default { components: { customTabbar }, mixins: [tabbarMixin] }; /script这样每个 tab 页面都引入同一个 mixin只要页面 onShowactiveTabPath 就会更新tabBar 组件根据 prop 推导出高亮项。2.3 中间“凸起按钮”等特殊样式的实现方式自定义 tabBar 最大的优势之一就是可以做特殊造型。最常见的是中间一个“发布”按钮比普通 tab 大一圈而且凸起。实现方式其实非常简单在 tabBar 组件里单独判断索引即可。template view classtabbar view classtabbar-item v-for(item, index) in tabbarList :keyindex clickhandleClick(item, index) view v-ifitem.isSpecial classtabbar-item-special image classtabbar-item-special-icon :srcitem.icon / /view template v-else image classtabbar-item-icon :srcactivePath item.path ? item.activeIcon : item.icon / text classtabbar-item-text{{ item.text }}/text /template /view /view /template样式方面中间特殊按钮用 transform: translateY(-10px) 往上偏移配合圆角和阴影就能做出“凸起”效果。需要注意的还是一样的问题凸起部分可能会被页面内容遮挡所以要给特殊按钮所在的容器设置高于普通 tabBar 的 z-index。点按特殊按钮时一般不会做页面切换而是弹出操作面板或者跳转一个非 tab 页面。这个逻辑可以在 handleClick 里通过 isSpecial 判断。3. 切换选项卡页面闪烁的根因分析与解决3.1 闪烁现象的现场还原自定义 tabBar 做好之后我遇到了那个让所有 uniapp 开发者头疼的问题页面切换闪烁。具体表现是从首页切到分类页或者从分类页切回首页屏幕底部会出现一个很短促的白色“闪动”有时候是整个页面内容白一下有时候是底部导航栏区域闪一下有时候是图片密集的页面闪得特别明显。这个问题在 H5 端偶尔出现在 App 端比较明显在小程序端真正严重——因为小程序 WebView 的渲染机制和页面切换机制跟 H5、App 都不太一样。刚开始我以为是页面加载慢的问题于是加了各种 loading 状态没用。又以为是图片太大的问题做了图片压缩还是有闪。最后我把问题拆解成两个方向才找到突破口。3.2 闪烁问题背后的三个深层原因第一个原因是页面栈切换导致的“白屏窗口期”。uniapp 的 tab 页面切换本质上还是多页面之间的切栈。当你调用 uni.switchTab 时目标页面会经历“创建 WebView → 加载页面结构 → 执行逻辑 → 渲染完成”这个过程。在这个过程完成之前屏幕上有短暂的时间是空白的或者只有底部导航栏如果你的导航栏是全局组件甚至在部分机型上上一个页面已经退场、下一个页面还在入场中间会出现一个“真空期”。这就是闪烁的第一个来源也是最根本的原因。原生 tabBar 之所以不闪是因为 tabBar 本身由原生渲染不受 WebView 加载影响切换时原生 tabBar 一直在用户感觉不到“页面框架空了”。自定义 tabBar 之后tabBar 也是页面的一部分页面加载期间 tabBar 和内容一起消失或空白闪烁感自然就来了。第二个原因是“可滚动容器高度突变”。很多自定义 tabBar 的项目里页面内容是滚动区域。从 A 页面切到 B 页面如果 B 页面内容比较短滚动区域没有被撑开页面高度就会跟 A 页面不同。在 WebView 或小程序渲染层里这会引起 scroll-view 或页面的重排重绘视觉上表现为内容跳动或闪烁。第三个原因是“图片资源的重新加载”。tab 页面通常都有图片尤其是电商项目列表页、首页密密麻麻全是图。页面每次切换时如果图片没有走缓存或者图片路径不是网络 URL 而是本地静态资源WebView 可能不会立即从内存缓存里取图而是重新走一遍加载流程。在图片加载完成的瞬间页面会从一个“无图状态”变为“有图状态”视觉上就是闪一下。3.3 解决闪烁的思路让“切换”变成“隐藏”解决闪烁问题的核心思路是不要频繁销毁和重建页面而是让页面“隐藏”和“显示”。原生 tabBar 能做到不闪也是同样的道理——tab 页面常驻切换只是前端显示层的切换不会重新加载。在 uniapp 里要实现这个效果最直接的手段就是使用 v-show 或 keep-alive 缓存页面状态。但 uniapp 的多页面结构里不能简单地在某一个页面里用 v-show 去控制其他页面。那怎么办我的方案是把 tab 页面改成“单页 多组件”的结构。也就是让 tabBar 的四个 tab 都作为组件存在于同一个页面比如首页入口用 v-show 控制它们的显示隐藏。具体做法是这样的pages.json 里只注册一个 tab 页面其他三个 tab 的内容不再注册为独立页面而是作为组件写进这个 tab 页面里。这个 tab 页面通过 uni.switchTab 切换时真正发生的是“切换了 tabBar 的选中项”但页面本身不销毁组件还挂在内存里。组件之间通过 v-show 切换显示因为组件一直存在不存在重新渲染的过程所以不会有白屏闪烁。但是这个方案有个很大的限制如果业务上必须使用独立的页面比如每个 tab 页面有自己的导航栏、有自己的页面生命周期逻辑、或者页面结构非常复杂把页面改成组件会带来很大改动。所以我这里介绍另一个更通用、还要解决“页面级闪烁”的方案把自定义 tabBar 设计为“全局覆盖层”让 tabBar 的高亮和切换逻辑完全脱离页面渲染同时利用 uni.switchTab 的页面缓存特性降低闪烁发生的概率。在 uniapp 中uni.switchTab 本身是有缓存机制的。如果你从 tab1 切到 tab2再切回 tab1tab1 页面并不要重新执行 onLoad而是直接触发 onShow。页面实例是缓存的DOM 也在。但在小程序端由于渲染层和逻辑层是分离的页面切栈时渲染层可能会短暂地“重新挂载组件”这是平台机制决定的开发侧很难完全规避。针对这个平台层面的“闪”我的实践是把图片资源做成本地化或绝对地址避免切换页面时重新请求图片同时在 tabBar 组件上做“切换过渡”的 CSS 动画用视觉上的“顺滑感”掩盖底层的短促闪烁。这几种方法配合起来我的项目里闪烁问题基本消失了。虽然不敢说 100% 完美解决但在真机测试里用户感知已经非常轻微。4. 页面缓存的落地方式与性能取舍4.1 v-if 和 v-show 的选择指南在自定义 tabBar 的场景里v-if 和 v-show 的选择非常关键。很多人知道 v-show 不会销毁组件、v-if 会但不知道这背后对页面性能的影响到底有多大。v-if 是惰性的条件为 false 时组件连渲染都不会发生性能好但切换时有“创建”的过程v-show 是控制 display 切换组件第一次渲染后就会一直存在切换时没有重新创建的消耗但缺点是一开始所有组件都会渲染。如果做“单页 多组件”的 tab 方案我建议首次启动时只渲染第一个 tabv-show 之外再加一层首次渲染标记其他 tab 等用户第一次点击时再初始化“持久化渲染”。简单来说就是首次用 v-if 创建创建后改成 v-show 常驻。实现思路是这样template view classtab-page view v-showcurrentTab 0 homeInited home-page v-ifhomeInited/home-page /view view v-showcurrentTab 1 categoryInited category-page v-ifcategoryInited/category-page /view /view /template script export default { data() { return { currentTab: 0, homeInited: true, categoryInited: false, cartInited: false, mineInited: false }; }, methods: { switchTab(index) { const initedKey [homeInited, categoryInited, cartInited, mineInited][index]; if (!this[initedKey]) { this[initedKey] true; } this.currentTab index; } } }; /script第一次点击“分类”时categoryInited 变为 true组件才会创建。之后再用 v-show 控制显示切换不会重建组件。这套逻辑兼顾了首屏性能和切换流畅性。4.2 页面数据与滚动位置如何保持自定义 tabBar 换页后如果页面数据没了、滚动位置丢了那体验还不如原生 tabBar。如果你用的是“多页面 uni.switchTab”的方案页面数据天然保持因为页面是缓存的。只需要在 onShow 时做数据刷新策略比如列表页每次显示时如果下拉刷新时间距上次超过 5 分钟自动重新拉取数据。表单页每次显示时保持原数据不做重置。如果你用的是“单页 多组件”的方案那就需要自己做状态保持每个 tab 组件的 data 本身就是独立的只要组件不销毁数据就在。滚动位置恢复需要在 tab 组件内部用 scroll-view 绑定 scroll-top或者在组件 onShow这里其实是组件显示时的回调时手动把滚动容器的 scrollTop 设置为之前保存的值。我一般这么处理滚动恢复template scroll-view :scroll-topscrollTop scrollhandleScroll classtab-scroll !-- 列表内容 -- /scroll-view /template script export default { data() { return { scrollTop: 0, scrollY: 0 }; }, activated() { this.scrollTop this.scrollY; }, methods: { handleScroll(e) { this.scrollY e.detail.scrollTop; } } }; /script这样每次切回这个 tab 时滚动条会自动恢复到上次的位置。4.3 首屏加载策略避免所有页面一次性渲染既然用 v-show 保持组件常驻那一个很直接的问题就是四个 tab 页面如果都很大首屏会不会卡死答案是对会卡。如果你一开始就把四个 tab 都渲染出来每个 tab 里是长列表、大量图片首屏加载时间会非常难看。所以必须做“懒初始化”也就是上面提到的点击时才首次创建组件。这个策略在用户第一次点击“我的”时可能那一下会有一两百毫秒的延迟但相比首屏卡顿这个取舍是值得的。更深一步的优化是把首页放在页面级其他 tab 用组件懒加载或者反过来页面级的 tab 保留 uni.switchTab 切换组件级的不保留。这个看具体业务没有绝对最优的方案核心是要理解每种方案的成本。5. switchTab 切换的监听与联动处理5.1 切换后如何刷新页面数据自定义 tabBar 用 uni.switchTab 切换页面时页面 onShow 会触发但 onLoad 不会重复触发。所以如果你的页面需要在每次展示时刷新数据不要在 onLoad 里做要在 onShow 里做。这里有一个很容易犯的错误有人会在 onShow 里不加判断地拉数据导致切换回来时页面“闪一下 loading”体验很差。更好的做法是第一次进入时用缓存数据渲染onShow 时静默请求数据返回后再更新页面。onShow() { // 静默刷新不展示 loading this.fetchList({ showLoading: false }); }如果列表是上拉加载更多的场景onShow 刷新后还要注意页码状态如果数据有变化分页页码要重置为第一页如果没有变化要保持当前页码避免滚动位置和数据量不匹配。5.2 点击当前 tab 触发的特殊交互项目里还常遇到一个需求点击当前已经在显示的 tab比如在首页再点一下首页按钮要让页面列表回到顶部。这个交互用自定义 tabBar 做起来非常方便。在自定义 tabBar 组件的点击事件里判断当前点击的 path 是否等于 activePath如果相等就触发一个自定义事件父页面监听后执行滚动到顶部等方法。!-- custom-tabbar.vue -- view classtabbar-item v-for(item, index) in tabbarList :keyindex clickhandleTabClick(item) /viewmethods: { handleTabClick(item) { if (this.activePath item.path) { // 点击的是当前 tab触发二次点击事件 this.$emit(tabbar-again, item); return; } uni.switchTab({ url: item.path }); } }父页面custom-tabbar :active-pathactiveTabPath tabbar-againhandleTabbarAgain /custom-tabbarmethods: { handleTabbarAgain() { // 滚动到顶部 uni.pageScrollTo({ scrollTop: 0, duration: 200 }); } }这个交互在电商项目里很实用用户逛了半天列表点 tab 能快速回顶体验比原生 tabBar 还好。5.3 底部导航栏与页面生命周期的协作这个知识点很容易被遗漏但真的是自定义 tabBar 的“命门”。uniapp 页面有 onLoad、onShow、onReady、onHide 等生命周期。自定义 tabBar 组件挂载在页面上后组件的 created、mounted 只会在页面第一次加载时执行。如果你在组件的 created 里做了初始化操作比如设置当前选中 tab当从页面 A 切到页面 B 时页面 B 的组件创建并初始化页面 A 的组件不会重新执行 created。如果你想让每次页面显示时组件都做点事情比如重置 tab 状态、更新角标、请求未读消息就不能依赖组件自身的生命周期要在页面的 onShow 里给组件传参或调用方法。具体做法是用 ref 调用子组件方法template view classtabbar-page custom-tabbar reftabbarRef :active-pathactiveTabPath /custom-tabbar /view /template script export default { onShow() { this.updateTabbarBadge(); }, methods: { updateTabbarBadge() { // 页面每次显示时主动调用组件方法更新角标 this.$refs.tabbarRef.updateBadge(2, 5); } } }; /script这样能保证每次页面展示时底部导航栏的状态都是最新的不会出现“角标显示残留旧数据”的问题。6. 深入排查隐藏的坑与真机调试经验6.1 自定义 tabBar 的层级与安全区问题自定义 tabBar 的层级问题经常在真机上才暴露出来。在部分安卓机型上如果 tabBar 组件没有设置足够高的 z-index页面里的弹窗、气泡、悬浮按钮可能会盖住 tabBar或者在 tabBar 之上漂浮看起来非常奇怪。我习惯给 tabBar 根节点设置 z-index: 999同时给页面内容区域设置一个低于这个值的 z-index。对于真正的全屏弹窗弹窗遮罩层要设置更高的 z-index比如 1000才能盖住 tabBar。再说安全区。iPhone 全面屏底部有 home indicator 区域如果 tabBar 直接贴在底部会被手势条挡到。解决方案是给 tabBar 底部加上安全区 padding.tabbar { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }同时tabBar 的高度计算也要把安全区算进去。如果 tabBar 设置了固定高度 50px在 iPhone X 上还硬要加 safe-area-inset-bottom会导致 tabBar 实际高度超过 50px页面预留的 padding-bottom 就不够了。我一般是把整体高度设为 50px safe-area-inset-bottom给 tabBar 设置 flex 布局让子元素在安全区分界线之上排列。6.2 页面切换的白屏问题真机与开发者工具的差异还有一点要特别提醒开发者工具里的表现和真机上的表现两个完全不一样。在微信开发者工具里自定义 tabBar 切换页面基本不闪因为工具的渲染机制和真机 WebView 不一样。但真机上特别是安卓“白屏闪烁”很常见。我的排查步骤是这样的第一步直接在真机上跑打开开发者工具的调试模式把渲染层的 console 输出打开看看切换页面时有没有组件报错。第二步检查是否用了原生组件video、map、canvas 等。这些原生组件在页面切换时会有更高概率出现闪烁因为它们和页面不在同一个渲染层。此时可以用 cover-view 包一层做过渡或者延迟加载原生组件。第三步检查页面图片。把本地静态图片改成网络图片走 CDN 缓存或者反过来把网络图片改成 base64 内联测试哪种方式在你的项目里表现稳定。没有绝对的标准答案不同项目差异很大。6.3 常见问题速查表问题现象可能原因解决办法切换 tab 后底部导航栏高亮错误页面 onShow 后未更新 activePath用 mixin 统一处理页面 onShow重新获取当前页面路径tabBar 被页面内容遮挡z-index 未设置或设置过低给 tabBar 设置 z-index: 999弹窗层级单独控制iPhone 底部被手势条遮挡未考虑安全区使用 env(safe-area-inset-bottom) 做 padding-bottom切换页面后列表滚动位置丢失页面或组件被销毁重建使用 v-show 保持组件scroll-view 记录 scroll-top切换时页面白屏页面图片重新加载或页面初始化过慢图片资源预加载配合静默刷新策略tabBar 角标数据不更新组件生命周期未触发父页面 onShow 时通过 ref 调用组件行为点击凸起按钮无反应凸起部分超出组件边界被裁剪检查父容器 overflow 属性不要设置为 hidden二次点击当前 tab 无回顶未监听当前 tab 的点击事件在组件点击事件中判断 activePath 是否相同切换页面时底部闪烁页面切换与 tabBar 渲染不同步使用页面缓存 图片预加载方案真机正常但工具卡顿开发者工具与真机渲染机制不同优先以真机调试验证工具仅作参考7. 进一步优化预加载、离屏渲染与体验升级7.1 图片预加载策略tab 页面切换闪烁的一个重要原因是图片加载。为了尽量缓解我在项目里做了一个简单的图片预加载管理器。在 App.vue 的 onLaunch 里把 tab 页面里可能用到的核心图片底部导航栏的图标、首页首屏的 banner 图等提前加载。// App.vue onLaunch() { // 预加载关键图片 const images [ /static/tabbar/home-active.png, /static/tabbar/category-active.png, /static/banner/main-banner.png ]; images.forEach(src { uni.getImageInfo({ src, complete: () {} }); }); }uni.getImageInfo 会触发图片的缓存流程页面真正用到这些图片时WebView 可以直接从缓存中读取减少白屏窗口。注意预加载不要做太多图片否则 app 启动时会抢占带宽反而拖慢首屏。7.2 离屏渲染 tab 页面如果你是在 App 端开发还有一个进阶操作使用 plus.nativeObj.View 或者 web-view 的离屏渲染来做 tab 页面的过渡。但这个方案复杂度较高需要引入原生的渲染配置一般业务项目不太推荐。多数情况下做到“页面缓存 图片预加载 v-show 保持”已经能满足用户感知了。7.3 配置 manifest 的一些注意点热搜词里提到了 manifest 配置这点在自定义 tabBar 场景下很容易被忽略。如果你用到了一些特殊的功能比如跨域请求、App 端调试等manifest.json 里的配置会影响页面加载和渲染速度。举例来说小程序端如果开启了“上传代码时自动压缩图片”开发环境和正式环境的图片加载速度可能差异很大。如果正式变慢可以检查是否开启了过狠的压缩策略。还有一点如果你的 App 端页面里用了 uni.webview.js 相关的功能需要确认 manifest.json 里是否包含了对应的模块配置否则切换到特定页面时可能会白屏或报错。8. 个人实测体验与最终建议自定义 tabBar 这件事做之前觉得只是“把原生的换成自己的”做之后才发现它背后牵扯的是页面结构、缓存策略、生命周期管理、图片加载优化、安全区适配这一连串问题。我在项目里最终定型的方案是pages.json 保留原生页面注册不配置 tabBar每个 tab 页面引入自定义 tabBar 组件用 mixin 统一处理 activePath页面内的主要数据在 onLoad 加载一次onShow 静默刷新图片资源做预加载tab 切换的闪烁问题基本消除用 ref 方式支持父页面主动更新 tabBar 的角标和特殊状态二次点击当前 tab 回顶功能产品经理反馈很好。如果你想直接抄作业那我建议从“多页面 uni.switchTab 自定义 tabBar 组件”这个方案开始。它和原生 tabBar 的交互逻辑最接近改动成本相对低而且遇到问题时社区里能搜到的经验也最多。像“单页 多组件”的方案虽然能更好地解决闪烁问题但如果你项目已经成型了改造成本很高除非新项目一开始就这么设计否则别轻易动。最后分享一个实操小技巧真机调试时把开发者工具的“模拟操作”和真机渲染对比着看。很多闪烁和层级问题工具上是看不出来的只有真机才能反映真实情况。我踩过的坑十有八九都是在真机调试的时候才暴露出来的。所以不要省真机调试那点时间尤其是自定义 tabBar 这种跟渲染层强相关的功能。
返回列表