uni-app 自定义底部导航栏实战:组件化、路由切换与安全区适配
简介这份zip源码面向uniapp开发者采用Vue语法实现自定义底部导航栏能够解决小程序、App等多端底部导航定制与样式适配难题适合中初级前端学习者参考也可供需要快速搭建自定义布局的开发者直接借鉴。压缩包内共23个文件涵盖vue页面文件负责导航结构、js逻辑脚本处理点击切换与交互、json页面配置、css样式表以及html预览页面整体大小仅122KB结构精简方便直接对照学习和迁移修改。当前已有213人学习下载。资源不仅展示了底部导航栏从配置到渲染的完整链路还包含点击切换、动态样式、图标与文字组合等实现细节附带的说明文档可帮助理清目录结构和运行方式既能作为课堂项目参考也能在此基础扩展实现更多自定义导航效果例如结合多端适配思路调整选中态与动画是一份轻量实用的uni-app源码范例。1. uni-app 自定义底部导航栏为什么值得自己写一套在 uni-app 项目里做自定义底部导航栏很多人第一反应是去 pages.json 配一项 tabBar跑通很容易可需求一旦变成动态角标、中间凸起按钮、按角色显示或隐藏某个 tab原生 tabBar 就开始拖后腿。换成 Vue 语法手写一套导航组件后导航栏就从框架能力降级成普通视图交互逻辑、样式、数据来源全部收归业务代码H5、微信小程序和 App 端还能保持同一套表现。这篇实战笔记基于一份 uni-app 自定义底部导航栏项目源码整理覆盖组件写法、路由切换、安全区适配和常见报错适合正在做跨端改版或想绕开原生限制的开发者参考。2. 拆开原生 tabBar 的边界为什么必须组件化2.1 原生 tabBar 的边界与自定义方案选型原生 tabBar 的优势是零代码、首屏快、自带页面切换缓存短板也在它自己身上icon 只能用本地静态图片tab 数量固定无法在中间放一个凸起的“发布”按钮动态角标能力在部分端上还受版本限制。早期版本的 uni-app 里想隐藏某一个 tab 只能改 pages.json 后重新编译运行期无法控制这在实际业务中基本是劝退项。等到需要权限控制、运营配置或者特殊样式时开发通常会转向自定义组件方案。自定义导航组件在项目中的定位是业务组件而非框架能力因此设计时要把数据源、路由跳转和样式完全解耦。我的建议是 items 数组只描述“长什么样、点哪里”选中态由页面在 onShow 里同步避免组件内部与页面互相引用。下面这张表是几种常见方案的适用范围方便按项目现状对号入座方案适用场景主要限制原生 tabBar图标固定、tab 固定、无角标需求无法中间凸起、动态增删 tabcover-view 覆盖只改视觉效果不改逻辑H5 端还原度差自定义组件 页面占位角标、凸起、权限控制、复杂样式需要处理安全区与路由状态插件市场现成组件赶进度接受样式依赖深度定制时改造成本高看起来是简单的底部导航放到跨端项目里会牵扯路由缓存、页面栈和样式隔离先把边界想清楚比写完再返工要省得多。2.2 页面结构与路由配置把 tab 页当成普通页面注册自定义导航的前提是放弃原生 tabBar也就是 pages.json 里不写 tabBar 字段把原本的 tab 页面注册成普通页面。如果项目里已经有原生 tabBar 配置先删掉否则页面底部会出现原生 bar 与自定义组件叠在一起的双底栏这是一个很明显的实现瑕疵。{ pages: [ { path: pages/home/index, style: { navigationBarTitleText: 首页 } }, { path: pages/category/index, style: { navigationBarTitleText: 分类 } }, { path: pages/publish/index, style: { navigationBarTitleText: 发布 } }, { path: pages/cart/index, style: { navigationBarTitleText: 购物车 } }, { path: pages/mine/index, style: { navigationBarTitleText: 我的 } } ] }这些页面变成普通页面后跳转不能再使用 uni.switchTab否则会报找不到 tabBar 页面。常规做法是首页、分类、购物车、我的这类低频栈页面用 uni.reLaunch发布这种功能页用 uni.redirectTo。两者的差别在于 reLaunch 会关闭所有页面再打开目标页页面栈只积累一层redirectTo 只关闭当前页保留进入前的页面栈适合返回链条清晰的模块。页面注册顺序同样有影响小程序端首屏页面放在 pages 数组第一位能减少启动时的页面加载耗时。2.3 数据驱动组件里的选中态从哪来导航组件内部不需要知道业务细节只要接收一个 items 数组和一个 current 索引。items 的每一项包含 pagePath、iconPath、selectedIconPath、text需要扩展时追加 badge、openType 字段即可。数据源可以是静态常量也可以来自 Vuex 或 Pinia 的全局状态但只要多个页面共用就要保证状态来源单一。template view classcustom-tabbar view v-for(item, index) in items :keyitem.pagePath classtabbar-item :class{ active: currentIndex index } taphandleTabClick(item, index) image classtabbar-icon :srccurrentIndex index ? item.selectedIconPath : item.iconPath / text classtabbar-text{{ item.text }}/text /view /view /template script export default { name: CustomTabbar, props: { items: { type: Array, required: true }, current: { type: Number, default: 0 } }, data() { return { currentIndex: this.current }; }, watch: { current(val) { this.currentIndex val; } }, methods: { handleTabClick(item, index) { if (index this.currentIndex) return; if (item.openType reLaunch) { uni.reLaunch({ url: item.pagePath }); } else { uni.redirectTo({ url: item.pagePath }); } } } }; /script这段代码里的关键逻辑有三处props 传入的 current 只作为初始值和外部同步入口组件内部维护 currentIndex 是为了让点击响应更跟手watch 监听 current 是为了支持非点击场景下的选中态变化比如从支付完成页返回时外部重新指定当前 tabhandleTabClick 里根据 openType 区分 reLaunch 和 redirectTo避免所有 tab 都走同一种跳转导致页面栈不可控。图标切换用三元表达式实时替换 src比同时渲染两个 image 再切换 display 更省性能。3. Vue 语法实现从组件模板到页面接入的完整源码3.1 模板与样式fixed 定位和安全区适配导航栏视觉上要固定在底部样式上用 position: fixedleft、right、bottom 置 0。高度一般取 100rpx对应 50px具体以设计稿为准。真正的坑是 iPhone 全面屏底部的手势条区域需要在组件根部加上 safe-area 的 padding 处理否则最后一个 tab 的文字会被手势条压住。template view classcustom-tabbar view classtabbar-content block v-for(item, index) in items :keyitem.pagePath view classtabbar-item :class{ active: currentIndex index } taphandleTabClick(item, index) view classtabbar-icon-wrap image classtabbar-icon :srccurrentIndex index ? item.selectedIconPath : item.iconPath modeaspectFit / text v-ifitem.badge classtabbar-badge{{ item.badge }}/text /view text classtabbar-text{{ item.text }}/text /view /block slot namecenter/slot /view /view /template style scoped .custom-tabbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 999; background-color: #ffffff; box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06); } .tabbar-content { display: flex; height: 100rpx; align-items: center; justify-content: space-around; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); } .tabbar-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; } .tabbar-icon { width: 48rpx; height: 48rpx; } .tabbar-icon-wrap { position: relative; } .tabbar-badge { position: absolute; top: -10rpx; right: -16rpx; min-width: 28rpx; height: 28rpx; padding: 0 6rpx; background-color: #fa3534; color: #ffffff; border-radius: 14rpx; font-size: 20rpx; line-height: 28rpx; text-align: center; } /style关键参数集中在底部安全区处理padding-bottom 同时写 constant(safe-area-inset-bottom) 和 env(safe-area-inset-bottom)前者是 iOS 11 前后的兼容写法后者是标准写法两行缺一不可。height: 100rpx 会在不同屏幕上自动换算普通设计稿给 50px 高度乘 2 就是 100rpx。tabbar-badge 的最小宽度设成 28rpx 并配合 border-radius: 14rpx数字超过两位时会自动拉长成胶囊形状。3.2 跳转逻辑switchTab 不再可用后的正确姿势组件里跳转不能直接复用原生 tab 的 switchTab因为页面已经不再是 tabBar 页面。经验做法是给每一项配置 openType默认走 reLaunch发布、编辑这类需要保留返回栈的页面配 redirectTo。handleTabClick(item, index) { if (index this.currentIndex) return; const url item.pagePath; if (item.openType reLaunch) { uni.reLaunch({ url, success: () { this.currentIndex index; }, fail: (err) { console.error([CustomTabbar] reLaunch failed, err); } }); } else { uni.redirectTo({ url, fail: (err) { console.error([CustomTabbar] redirect failed, err); } }); } }成功回调里再更新 currentIndex而不是点击后立刻改是为了避免跳转失败时选中态已经变化页面和导航栏状态对不上。调试时看到 fail 回调第一反应是检查 pages.json 里的 path 是否带前导斜杠、文件名是否正确。另一个容易忽略的点是reLaunch 会清空页面栈从 tab 页跳详情页后想用返回回到前一个 tab 是做不到的这种场景要把详情页设计成普通页面跳转而不是 tab 入口。3.3 页面接入占位高度与原生导航栏隐藏页面底部要留出和组件等高的占位否则固定定位的导航栏会盖住内容。占位高度必须加上安全区高度iOS 上才不会被手势条遮住最后一行的操作按钮。接入时组件放在页面模板的最外层并传入当前页对应的 items 和 current。template view classpage view classpage-content text classpage-title首页内容区/text /view view classtabbar-placeholder/view CustomTabbar :itemstabbarItems :current0 / /view /template script import CustomTabbar from /components/custom-tabbar/index.vue; export default { components: { CustomTabbar }, data() { return { tabbarItems: [ { pagePath: /pages/home/index, iconPath: /static/tabbar/home.png, selectedIconPath: /static/tabbar/home-active.png, text: 首页, openType: reLaunch }, { pagePath: /pages/category/index, iconPath: /static/tabbar/category.png, selectedIconPath: /static/tabbar/category-active.png, text: 分类, openType: reLaunch }, { pagePath: /pages/publish/index, iconPath: /static/tabbar/publish.png, selectedIconPath: /static/tabbar/publish-active.png, text: 发布, openType: redirectTo }, { pagePath: /pages/cart/index, iconPath: /static/tabbar/cart.png, selectedIconPath: /static/tabbar/cart-active.png, text: 购物车, openType: reLaunch }, { pagePath: /pages/mine/index, iconPath: /static/tabbar/mine.png, selectedIconPath: /static/tabbar/mine-active.png, text: 我的, openType: reLaunch } ] }; } }; /script style scoped .page { min-height: 100vh; background-color: #f7f8fa; } .tabbar-placeholder { height: 100rpx; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); } /styletabbarItems 里的 pagePath 必须与 pages.json 注册的路径一致多一个斜杠或者少一个斜杠都会导致 reLaunch 失败。图标路径这里写成 /static/tabbar/xxx.png字符串形式在小程序端最稳。存在多个 tab 页面时建议把 tabbarItems 抽取到公共配置文件里各页面直接从配置引入避免每个页面各写一份改一个 tab 要全量替换。占位 view 的样式与导航栏高度保持同步组件高度一旦调整这里也要跟着改。样式值px 值说明100rpx50px导航栏高度48rpx24px图标宽高28rpx14px角标最小宽度20rpx10px角标字号这里的 rpx 和 px 换算基准是 750 设计稿宽度rpx 在宽屏设备上自动缩放做适配时直接用 rpx 写尺寸比手算 px 更稳。4. 进阶改造角标、凸起按钮与多端差异处理4.1 把 uni.setTabBarBadge 换成自己的角标渲染原生方案里给 tab 加角标要调用 uni.setTabBarBadge 并指定 index自定义组件方案直接在数据层控制。items 里预留 badge 字段值为 0 或空字符串时不渲染大于 99 时显示 99这个规则和很多消息列表的角标规范一致。setBadge(tabIndex, count) { if (!this.items[tabIndex]) return; this.$set(this.items[tabIndex], badge, count 99 ? 99 : String(count)); }用 $set 是为了让新增的 badge 字段具备响应式能力直接 this.items[tabIndex].badge xxx 在小程序端可能不会触发视图更新。调用方在父组件里通过 ref 拿子组件实例this.$refs.tabbar.setBadge(0, 5)。如果项目里同时残留原生 tabBarsetTabBarBadge 会和自定义角标各自渲染一个出现双角标叠影所以接入自定义方案时原生 tabBar 要从 pages.json 里彻底移除。需要在角标上支持小红点模式时可以再加一个 badgeType 字段。值为 dot 时只渲染 8rpx 的圆点值为 number 时走数字逻辑运营活动要的“有小圆点但不显示数字”也能在一个组件里兼容。4.2 中间凸起按钮slot 插槽与 z-index 控制中间按钮凸起一般分两种一种图标比两边稍大但并不超出导航栏另一种是整体按钮向上偏移并带自己的点击事件。后一种更适合用 slot 实现模板里预留一个 center 插槽默认内容显示“发布”按钮业务页面需要定制时可以覆盖。view classtabbar-center-slot taphandleCenterClick slot namecenter view classcenter-default image classcenter-icon src/static/tabbar/center.png / text classcenter-text发布/text /view /slot /view对应样式让插槽容器绝对定位于导航栏中间偏上.tabbar-center-slot { position: absolute; left: 50%; bottom: 30rpx; transform: translateX(-50%); z-index: 1000; width: 120rpx; height: 120rpx; display: flex; align-items: center; justify-content: center; }transform: translateX(-50%) 配合 left: 50%让按钮在任意宽度下都居中比 left 写死像素要稳。z-index 要比导航栏容器高但低于页面上常见弹层的层级否则弹层出现时中间按钮会穿透显示。点击区域不要小于 88rpx真机上手指触摸区域过窄容易误触相邻 tab。H5 端这段定位没有兼容问题小程序端注意不要在插槽外层使用 overflow: hidden否则凸出的部分会被裁掉。4.3 多端差异H5、微信小程序与 App 的表现差异同一套组件在不同端的差异集中体现在安全区、图片渲染和路由转场上。H5 端 env(safe-area-inset-bottom) 在部分安卓浏览器里不生效需要额外的媒体查询兜底小程序端的 image 组件默认有 320px 宽度的上限必须显式设置 width 和 heightApp 端使用 vue 页面时页面切换自带原生转场导航栏的选中态如果不在 onShow 里及时同步切换回来时会看到选中态闪一下再归位。端安全区图片渲染切换动画H5env() 部分安卓不生效CSS 控制即可无原生转场微信小程序部分机型返回值偏大需显式宽高转场由基础库控制App需结合 manifest 屏幕适配网络图可缓存原生转场易造成闪烁建议每个 tab 页面都在 onShow 里同步当前索引不要在 onLoad 里只设置一次。因为从普通页面返回 tab 页时 onLoad 不一定执行onShow 每次显示都会触发。同步代码就一行通过当前页面路由匹配 items 下标匹配成功后把 current 传给组件。对于做过多端项目的团队还建议把这一行同步逻辑放到页面公共 mixin 里避免复制到每个页面后各写各的。5. 排错与验证照着这几步排查自定义导航栏问题5.1 真机底部被 iPhone 手势条遮挡检查 .tabbar-content 是否同时写了 constant(safe-area-inset-bottom) 和 env(safe-area-inset-bottom)且占位 view 的高度与组件保持一致。遗漏的情况下导航栏内容被手势条压住但页面内容未必会被遮挡因为 fixed 定位与占位是两条独立的渲染路径。5.2 页面切换后导航栏选中态不对优先检查当前页面的 onShow 是否覆盖了组件 current。不要依赖组件内部的 currentIndex 自增用户可能通过左上角返回、扫码或推送消息进入任意页面导航栏必须根据当前路由反推选中索引。onShow() { const pages getCurrentPages(); const currentPage pages[pages.length - 1]; const route /${currentPage.route}; const idx this.tabbarItems.findIndex((item) item.pagePath route); if (idx -1) { this.currentIndex idx; } }这段代码放在页面 data 对应的脚本里核心是 route 与 pagePath 的精确匹配。getCurrentPages 在小程序端拿到的是页面栈App 端同样适用如果匹配不到索引说明 pages.json 中的 path 与 tabbarItems 中的 pagePath 不一致去查斜杠和大小写即可。5.3 图标不显示或显示 404先看路径是否以 /static/ 开头相对路径在小程序端经常解析失败。再看图片格式svg 在小程序部分基础库版本不被 tab 图标支持webp 也有版本门槛统一转成 png 或 jpg 最省事。H5 端还涉及跨域图片服务器没有开启 CORS 时控制台会直接报加载失败。5.4 用构建命令验证产物HBuilderX 里可视化运行时不易接力 IDE 工具链建议保留 npm 脚本用于验证。微信小程序端跑 npm run dev:mp-weixin产物生成到 dist/dev/mp-weixin打开微信开发者工具导入该目录即可。验收时逐项过这张表验证项通过标准pages.json 是否移除 tabBar 字段无原生底栏残留每个页面占位高度是否正确最后一屏内容不被遮挡currentIndex 与路由是否匹配切换后选中态不变安全区两条 padding 是否齐全iPhone 手势条不压导航栏图标资源是否为 png/jpg控制台无 404按这张表逐项过一遍基本能在十分钟内定位自定义导航栏的常见问题。最后提醒一个细节微信开发者工具里模拟 iPhone 13 与真机表现经常有偏差安全区相关问题建议以真机为准。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →