简介:这份资源是一套微信小程序自定义顶部导航栏的完整实例,面向需要兼容适配所有机型、希望替换原生导航栏效果的小程序开发者或前端学习者。核心思路是先在全局配置中隐藏系统导航栏,再通过自定义组件实现胶囊按钮对齐、状态栏高度适配等细节,同时涵盖组件用法、参数传递及导航栏相关配置要点,可直接套用到真实项目。压缩包为 rar 格式,共十六个文件,体积仅约九 KB,非常轻量。其中以 json、js、wxss、wxml 文件为主,分别承担页面配置、逻辑处理、样式定义和结构渲染,另含两张示例图片便于对照效果。整体目录结构清晰,包含全局配置、页面、组件及工具模块,适合按模块阅读。目前已有三千三百七十人学习下载,该实例虽小但针对性强,能帮助开发者快速理清自定义导航栏的实现路径,避开机型适配中的常见问题,是一份即拿即用的参考代码。
1. 自定义顶部导航栏:navigationBar 不够用时的第一选择
收到过一个需求:小程序顶栏要改成品牌色,标题左边还要放一个小图标。默认的 navigationBar 只允许改背景色、文字颜色,图标、插槽、滚动变色、渐变遮罩统统不支持。于是只能把navigationStyle设为custom,关掉原生导航栏,用 view 自己画一条。标题里讲的这件事,本质就是:拿到状态栏高度、用胶囊按钮反推导航栏高度、把这段逻辑封装成组件,并处理灵动岛、刘海屏、状态栏高度返回 0 这类边界。适合所有需要定制顶部导航栏的小程序开发者,新手可以直接抄组件,熟手可以拿走一套连边界问题都覆盖的测量方案。
2. 用 getMenuButtonBoundingClientRect + statusBarHeight 推导导航栏高度
2.1 微信把胶囊放在哪,导航栏就有多高
微信小程序的顶部由两段组成:状态栏和导航栏本体。状态栏是系统绘制的那一条,显示时间、信号、电量;导航栏本体是微信绘制的那一条,胶囊按钮(右上角那三个点)就嵌在其中。默认情况下,胶囊按钮在导航栏内是垂直居中的,这给了我们一个反推公式的机会。
// 取胶囊按钮在屏幕中的位置和尺寸 const menuRect = wx.getMenuButtonBoundingClientRect() // 取状态栏高度(注意兼容新老 API) const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() const statusBarHeight = info.statusBarHeight // 导航栏本体高度 = (胶囊顶部到状态栏底部的距离) * 2 + 胶囊高度 const navBarHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height这个公式的前提是胶囊垂直居中于导航栏。上间距等于下间距,所以导航栏本体高度就是“上间距 + 胶囊高度 + 下间距”。menuRect.top - statusBarHeight是胶囊上边缘到状态栏底部的距离,乘以 2 就是上下两段的等距空间,再加上胶囊自身高度,正好是导航栏本体的完整高度。
这套推导并不是为了算得精确到像素,而是为了让整个导航栏在视觉上和胶囊按钮保持“左右平衡”。如果你写死了 44 或 48,在部分 Android 机上胶囊偏上,标题就会显得偏下;用公式推导,标题永远和胶囊对齐。常见答疑里“微信小程序顶部导航栏高度是多少”就没法给固定答案,iOS 一般是 44,Android 一般是 48,但刘海屏、灵动岛、字体缩放都会改变这两个数字,公式才是跨机型通用的解法。
2.2 第一次取不到胶囊坐标?先同步后延迟
wx.getMenuButtonBoundingClientRect()在基础库 2.1.0 之后可用,但“可用”不代表“第一次调用就一定拿得到准确值”。在部分 Android 机型上,页面刚渲染时胶囊的 rect 可能返回全 0 或 undefined,尤其是从分享卡片、扫码、外部跳转冷启动进入页面的时候。
我的做法是:同步取一次,取不到就用兜底常量,然后在页面onShow里延迟 100~200ms 再取一次,把准确的 rect 值覆盖进去。
const DEFAULT_MENU_RECT = { top: 26, height: 32, left: 278, right: 365, width: 87 } function getMenuRect() { try { const rect = wx.getMenuButtonBoundingClientRect() if (rect && rect.height > 0 && rect.top > 0) { return rect } } catch (e) { // 低版本基础库或某些 WebView 环境可能直接报错 } return DEFAULT_MENU_RECT }兜底常量选top: 26, height: 32能覆盖大部分竖屏机型,但不要依赖它作为最终值。延迟重取的意义在于:首次渲染时状态栏高度和胶囊位置可能还没被系统层回传,等 100ms 后微信绘制完成,拿到的值才是用户屏幕上的真相。
| 获取时机 | 可靠性 | 说明 |
|---|---|---|
| Page.onLoad 同步取 | 大多数情况可靠 | 冷启动偶发取到 0 |
| Component.attached 同步取 | 同上 | 组件实例化早于页面布局 |
| Page.onShow 延迟 200ms 取 | 最可靠 | 微信绘制完成后的最终值 |
| App.onLaunch 同步取 | 不可靠 | 此时导航栏尚未初始化 |
延迟重取只做一次,不要做成定时器,否则页面反复切换时会闪一下。新值和旧值差异小于 2 像素时直接忽略,避免无意义的 setData。
2.3 导航栏高度的 rpx / px 换算边界
很多人在这一步会踩坑:把导航栏高度按 rpx 写进 wxml。导航栏的高度、胶囊的位置、状态栏高度,getMenuButtonBoundingClientRect()和getWindowInfo()返回的全部是物理像素 px,不是 rpx。
<!-- 正确:px 直接用于 style --> <view style="height: {{statusBarHeight}}px; background: #fff;"></view> <view style="height: {{navBarHeight}}px; background: #fff;"></view>页面根节点拿到这两个值后,padding-top用statusBarHeight + navBarHeight撑开内容区域。不要把 px 转换成 rpx 再传进 style,微信的 style 绑定直接接受 px,转成 rpx 反而会导致不同屏幕宽度下高度失真。
3. 把高度计算封装成自定义导航栏组件
3.1 组件结构与 wxml 布局
第 2 章的公式和多时空值逻辑,直接写进页面会散落得到处都是。我一般会把导航栏封装成custom-nav-bar组件,页面 json 注册,wxml 里一行引入,组件的data里维护statusBarHeight和navBarHeight。
组件目录结构如下:
components/custom-nav-bar/ ├── index.js ├── index.json ├── index.wxml └── index.wxss先看 index.json:
{ "component": true, "options": { "multipleSlots": true } }multipleSlots: true是为了支持右侧插槽,后面放分享按钮或菜单按钮时要用到。组件 index.js 如下:
const DEFAULT_MENU_RECT = { top: 26, height: 32, width: 87, left: 278, right: 365 } Component({ options: { multipleSlots: true }, properties: { bgColor: { type: String, value: '#ffffff' }, title: { type: String, value: '' }, titleColor: { type: String, value: '#1a1a1a' }, showBack: { type: Boolean, value: true } }, data: { statusBarHeight: 20, navBarHeight: 44, capsuleWidth: 87, capsuleHeight: 32 }, lifetimes: { attached() { this.initMenuRect() } }, methods: { initMenuRect() { const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() const rect = this.getMenuRect() const statusBarHeight = info.statusBarHeight || 20 const navBarHeight = (rect.top - statusBarHeight) * 2 + rect.height this.setData({ statusBarHeight, navBarHeight, capsuleWidth: rect.width, capsuleHeight: rect.height }) }, getMenuRect() { try { const rect = wx.getMenuButtonBoundingClientRect() if (rect && rect.height > 0 && rect.top > 0) { return rect } } catch (e) {} return DEFAULT_MENU_RECT }, handleBack() { const pages = getCurrentPages() if (pages.length > 1) { wx.navigateBack({ delta: 1 }) } else { // 冷启动时没有上一页,跳回首页 wx.switchTab({ url: '/pages/index/index' }) } } } })initMenuRect()里先取窗口信息,再取胶囊 rect,最后算出导航栏高度。注意getWindowInfo()如果版本过低会不存在,所以写法是wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()。getMenuRect()单独抽出来,是为了后面在onShow里延迟重取时复用。
3.2 组件 wxml 的三种节点:占位层、固定层、内容层
组件模板的关键是把“占位”和“固定”分离。占位层在文档流里撑开高度,固定层用position: fixed悬浮在页面顶部,这样页面滚动时导航栏一直可见,占位层保证页面内容不会钻进导航栏下方。
<!-- 占位:把页面内容顶到导航栏以下 --> <view class="nav-placeholder" style="height: {{statusBarHeight + navBarHeight}}px; background: {{bgColor}};"></view> <!-- 固定:真正显示的导航栏 --> <view class="nav-bar" style="height: {{statusBarHeight + navBarHeight}}px; background: {{bgColor}};"> <view class="status-bar" style="height: {{statusBarHeight}}px;"></view> <view class="nav-content" style="height: {{navBarHeight}}px;"> <view class="nav-side" style="width: {{capsuleWidth}}px;"> <view wx:if="{{showBack}}" class="back-btn" bindtap="handleBack"> <view class="back-icon"></view> </view> </view> <view class="nav-title" style="color: {{titleColor}};">{{title}}</view> <view class="nav-side"> <slot name="right"></slot> </view> </view> </view>.nav-content用 flex 布局,左右各一个和胶囊等宽的.nav-side,中间.nav-title横向居中。这样标题的中心点落在左右两侧按钮区域的正中,不会被右侧胶囊按钮的重量带偏。
/* index.wxss */ .nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 999; } .nav-content { display: flex; align-items: center; padding: 0 8px; } .nav-side { display: flex; align-items: center; flex-shrink: 0; } .nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .back-btn { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } .back-icon { width: 12px; height: 12px; border-left: 2px solid #1a1a1a; border-bottom: 2px solid #1a1a1a; transform: rotate(45deg); }这里.nav-side左侧和右侧宽度相同,右侧槽位即便没有内容,也占据和胶囊等宽的空白,保证标题视觉居中。返回箭头没有用图片,而是用 CSS 画了一个‹形状,避免引入额外资源,颜色跟随titleColor则把 border 颜色也换成 data 绑定。
3.3 在页面里使用:三行接入
页面 json 注册组件:
{ "navigationStyle": "custom", "usingComponents": { "custom-nav-bar": "/components/custom-nav-bar/index" } }页面 wxml 引入:
<custom-nav-bar title="商品详情" bg-color="#f5f5f5" title-color="#333333" show-back="{{true}}" />页面无需再写任何高度计算逻辑,组件内部负责测量。唯一要注意的是:navigationStyle: "custom"要写进页面 json,如果写在 app.json 的 window 下,所有页面都会失去原生导航栏,到时候每个页面都要引组件,排查起来很麻烦。
4. 跨机型适配清单:从灵动岛到状态栏高度为 0 的坑
4.1 状态栏高度返回 0 或 20 的兜底策略
statusBarHeight在真机上通常不会为 0,但开发工具模拟器、部分 Android WebView 环境、以及极个别定制 ROM 上确实会出现 0。另一种情况是返回固定 20,这是微信对无法识别状态栏高度的降级值,并非真实高度。
兜底方案要分两层:取值时兜底,布局时兜底。
const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() let statusBarHeight = info.statusBarHeight || 20 // 额外处理:iPhone 刘海屏机型最低 44,灵动岛最低 59 if (statusBarHeight < 20) { statusBarHeight = 20 }顺序是:先用新 API 取,取到后判断是否小于 20,小于就按 20 处理。20 是 iPhone 非刘海屏和大部分 Android 的基础状态栏高度,这个兜底值能保证布局不会塌掉,但不能保证视觉完美。不正常的高度意味着胶囊 rect 大概率也不正常,所以兜底之后还要做一次延迟重取校准。
4.2 灵动岛、横屏和胶囊位置的特殊情况
iPhone 灵动岛机型的状态栏高度在竖屏下是 59 或 62,胶囊按钮整体下移,用公式反推出来的navBarHeight会自动变大,不需要针对灵动岛单独写 if 判断。容易出问题的是横屏场景:横屏下状态栏高度变为 0,胶囊按钮会移动到屏幕左侧或右侧边缘。
小程序如果要支持横屏,自定义导航栏建议直接不做,或单独做一套横屏布局。判断横屏的代码:
const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() if (info.windowWidth > info.windowHeight) { // 横屏:降级为原生导航栏或抬高导航栏底部 this.setData({ navBarHeight: 32 }) }横屏的复杂点在于不同机型的胶囊位置差异大,有的在左,有的在右,公式推导出来的高度在横屏下没有参考价值。常规做法是横屏下强制使用默认导航栏,在页面 json 里动态切换navigationStyle不现实,所以绝大多数小程序直接放弃横屏自定义导航栏,只有游戏类小程序会用。
4.3 页面栈深度与返回键的显示逻辑
自定义导航栏的返回键不能一直显示。从首页进入详情页,页面栈长度是 2,返回键应该出现;从分享卡片冷启动进入页面,页面栈长度是 1,没有上一页可返回,返回键点了也没有反应。组件里已经处理过这个逻辑:
handleBack() { const pages = getCurrentPages() if (pages.length > 1) { wx.navigateBack({ delta: 1 }) } else { wx.switchTab({ url: '/pages/index/index' }) } }这里有个细节:冷启动进入的页面如果本身不是 tabBar 页面,switchTab会失败,需要加fail回调,或者判断当前页面是否属于 tabBar 页面再做跳转。常见做法是给组件加一个homeUrl属性,由使用方传入冷启动时的回跳地址。
4.4 字体缩放和 onShow 时的二次校准
微信「通用-字体大小」设置改变后,重新打开小程序,胶囊按钮的位置和状态栏高度可能发生变化。这不是每次都会发生,但一旦发生,导航栏标题和胶囊的垂直对齐就会偏差几个像素。解决方式是在页面onShow里延迟重取一次导航栏信息。
onShow() { setTimeout(() => { const navBar = this.selectComponent('#customNavBar') if (navBar) { navBar.initMenuRect() } }, 200) }重取时比较新旧数据差异,差距小于 2px 直接 return,不给用户看到闪烁。这行代码不用每个页面都写,我一般封装一个useCustomNavBar的混入或组件方法,页面只需要在 onShow 调一次。
| 机型/场景 | 现象 | 处理方案 |
|---|---|---|
| iPhone 灵动岛 | 状态栏高 59,胶囊下移 | 公式自动适配,无需判断 |
| Android 挖孔屏 | 状态栏高度 44~48 | 公式自动适配 |
| 开发工具模拟器 | 状态栏固定 20/24 | 真机预览为准 |
| 冷启动拿不到 rect | 导航栏高度错误 | onShow 延迟 200ms 重取 |
| 字体缩放后 | 胶囊与标题不对齐 | 重取后 diff 阈值更新 |
| 横屏 | 状态栏为 0,胶囊移动 | 放弃自定义或单独布局 |
5. 自定义导航栏的滚动渐变、状态栏前景色与机型走查
5.1 滚动渐变:从透明到纯白的过程
自定义导航栏常见需求是:页面顶部是头图,导航栏透明显示白色文字;往下滚动后,导航栏变成白色背景、黑色文字。这个效果不需要组件内部监听滚动,而是由页面通过onPageScroll把状态传给组件。
// 页面 js onPageScroll(e) { const { scrollTop } = e this.setData({ navBarScrolled: scrollTop > 50 }) }组件增加一个scrolled属性,wxml 里动态切换样式:
<view class="nav-bar {{scrolled ? 'nav-bar--scrolled' : ''}}" style="background: {{scrolled ? bgColor : 'transparent'}};">配合.nav-bar的transition: background-color 0.2s ease;,就能得到一个平滑的渐变效果。注意状态栏前景色(时间、电量、信号的颜色)也要跟着切换,页面 json 里navigationBarTextStyle支持black和white,在自定义导航栏模式下依然生效。滚动后调用wx.setNavigationBarColor切换前后景色:
if (scrollTop > 50) { wx.setNavigationBarColor({ frontColor: '#000000' }) } else { wx.setNavigationBarColor({ frontColor: '#ffffff' }) }frontColor只支持#000000和#ffffff,别传品牌色,否则会报错。切换时会有瞬间跳变,微信没有提供渐变动画,所以只在滚动跨过阈值时调用一次,不要在每个 scroll 事件里都调。
5.2 下拉刷新区域与导航栏的边界处理
用了自定义导航栏后,页面下拉刷新的动画会出现在导航栏下方,而不是原生导航栏下方。如果你页面里开了enablePullDownRefresh: true,胶囊按钮区域会露出默认的白色背景,非常难看。
处理办法有两个。一是把enablePullDownRefresh关闭,用 scroll-view 自定义下拉刷新;二是把页面根节点page的背景色设置成导航栏背景色相同,视觉上融为一体。推荐第一种,scroll-view 自带refresher-enabled,刷新动画的位置可以自由控制,不会和导航栏抢空间。
5.3 发布前的机型走查清单
每次发布涉及导航栏改动,我都会过一遍这个清单,比看任何文档都有用:
- 开发工具 iPhone X 模拟器 + 真机 iOS 刘海屏各跑一遍:确认标题上下间距一致,返回按钮不浮在状态栏里。
- Android 真机选一台挖孔屏手机(现在主流就是这类):检查状态栏高度和胶囊 rect 是否正常,重点看返回按钮和胶囊按钮的底部是否对齐。
- 把微信字体大小调到最大,杀掉小程序重新进入:确认二次校准生效,标题没有下移。
- 从分享卡片、小程序码、公众号菜单三种入口冷启动:确认页面栈长度判断正确,返回键不会出现在首页。
- 开启深色模式,切到深色背景页面:确认导航栏背景和状态栏前景色都有对应配置。
- 横竖屏切换一次,如果被设计上禁止,确认
pageOrientation为portrait,避免横屏下导航栏塌掉。
最后补一个细节:getSystemInfoSync()在小程序基础库 2.20.1 之后标注即将废弃,不建议继续使用。新项目直接wx.getWindowInfo(),老项目给兼容分支,不要图省事只用老 API,等基础库升级后你会在控制台看到各种 deprecation warning。
本文还有配套的精品资源,点击获取