简介:微信小程序原生顶部导航栏在样式定制和不同机型适配上有不少限制,这份完整实例正面向有自定义导航栏需求的小程序开发者,演示如何先在 app.json 中关闭原生导航栏,再通过自定义 navigationBar 组件构建统一头部,并处理好组件属性定义、参数传递、状态栏高度与胶囊按钮位置适配等关键环节,让导航栏在刘海屏、水滴屏及普通机型上都能保持稳定布局,完整代码可直接参考或二次改造。压缩包共16个文件,以 json、js、wxss、wxml 为主:json 用于页面和组件配置,js 负责导航栏逻辑与数据交互,wxss 控制视觉样式,wxml 搭建组件结构,另附2张png效果图便于预览,整包仅9KB,轻量且没有冗余资源,适合快速学习或嵌入现有项目。目前已有3370人学习下载,内容得到过不少小程序开发者的关注。通过这份实例,开发者能梳理出一套从原生导航栏隐藏、自定义组件封装、参数传入到多机型适配的完整思路,同时获得一个命名清晰、目录结构一目了然的 navigationBar 组件雏形,可直接用于后续项目复用。
1. 自定义顶部导航栏:为什么每个微信小程序项目都要重新处理
刚开始做微信小程序时,系统自带的navigationBar看起来够用:能改标题、背景色、前后按钮。但一旦页面顶部需要放自定义组件、做沉浸式效果,或者想统一安卓和iOS的视觉差异,默认导航栏立刻变得僵硬。最头痛的是不同机型的顶部安全区、胶囊按钮坐标和状态栏高度都不一样,写死固定px值在iPhone 14 Pro Max上刚好,换到华为Mate 60可能就会出现标题顶到状态栏、按钮错位。自定义顶部导航栏(navigationBar)就是把整个顶部区域接管过来,自己计算状态栏和胶囊按钮位置,从而兼容适配所有机型。这篇文章从微信官方提供的接口原理讲起,落到一个能直接复制使用的完整实例,适合正在做小程序项目或准备封装统一导航组件的人。
2. 自定义导航栏原理:状态栏、导航栏与胶囊按钮的高度如何计算
2.1 三个关键数据:safeArea、menuButtonRect与screenHeight
自定义顶部导航栏不能凭感觉写px,微信提供了一套官方接口用来获取设备和胶囊按钮的坐标。核心是wx.getWindowInfo()和wx.getMenuButtonBoundingClientRect()。前者的返回值里有safeArea、statusBarHeight、screenWidth、screenHeight;后者返回胶囊按钮的准确位置,包括top、bottom、left、right和宽高。这两个接口组合起来,就能描述任何机型下导航栏的工作区域。
实际计算导航栏高度时,常用公式是:
导航栏高度 = (menuButtonRect.top - statusBarHeight) * 2 + menuButtonRect.height
这个公式的原理是:微信在布局时,会让胶囊按钮的垂直中心对齐导航栏区域的中心。因此从状态栏底边到胶囊按钮顶部的距离,乘以2,再加上胶囊本身高度,刚好得到完整的导航栏高度。这样计算出来的值,在iOS和Android上都能自动跟随胶囊位置,而不是依赖一张固定的机型对照表。
2.2 为何不直接使用默认navigationBar
默认navigationBar在全局配置app.json里通过navigationStyle: custom就能关掉,但很多人不知道关掉后页面顶层会直接顶到屏幕最上方。此时状态栏文字仍然是原来的黑白色,如果页面背景也是白色,状态栏文字就会看不清。同时胶囊按钮仍然保留,它不是页面的wxml,而是微信原生绘制的,不能通过组件隐藏。所以自定义顶部导航栏的本质是:确定状态栏高度、导航栏自身高度、胶囊按钮的位置,然后把自己的view放在胶囊按钮同一行,中间留出合适间距。
这里给出一个常用的数据获取代码片段:
function getNavInfo() { const windowInfo = wx.getWindowInfo(); const menuRect = wx.getMenuButtonBoundingClientRect(); const { statusBarHeight, screenWidth } = windowInfo; const navHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height; return { statusBarHeight, navHeight, menuRect, screenWidth, navBarHeight: navHeight + statusBarHeight // 顶部总占用高度 }; }这段代码中,navHeight是从状态栏底部到导航栏底部的高度,通常范围在32到48px之间;navBarHeight则是整个顶部区域从屏幕顶部到导航栏底部的高度,页面内容设置padding-top时使用这个值。注意wx.getWindowInfo()从基础库2.20.1开始稳定,如果项目还在用老版本,可以加一个兼容判断,用wx.getSystemInfoSync()兜底。
2.3 状态栏文字颜色与背景穿透
自定义导航栏后,状态栏由页面窗口接管,需要通过wx.setNavigationBarColor来设置状态栏文字颜色,但注意当navigationStyle: custom时,这个接口只对状态栏前景色有效,背景色需要自己画。还有一种方式是直接使用page-meta组件中的page-style字段,例如:
<page-meta page-style="--status-bar-color: #ffffff;" />不过更通用的是在页面json里配置"navigationStyle": "custom",然后在布局中用一个占位view设置高度为statusBarHeight,背景色和导航栏一致,这样视觉上就形成了完整的自定义顶部导航栏。在安卓手机上,部分机型状态栏背景色会默认是黑色或半透明,需要把占位view背景色设置成不透明,否则会出现状态栏和页面背景不一致的尴尬情况。
3. 手写一个兼容所有机型的自定义顶部导航栏组件
3.1 在页面json中启用custom导航
所有实现的第一步是在对应页面的.json文件中配置:
{ "navigationStyle": "custom", "navigationBarTextStyle": "white" }navigationBarTextStyle在这里不影响布局,但可以提前把状态栏文字指定为白色,因为自定义导航栏通常是深色背景。如果导航栏是浅色,应该设置成black。注意这个设置是页面级的,页面切换时,状态栏文字颜色变化可能会晚一帧,如果出现闪烁,可以在onLoad里主动调用一次wx.setNavigationBarColor({ frontColor: '#ffffff' })。
3.2 组件结构设计:状态栏占位、导航栏容器、菜单栏按钮
我通常把自定义导航栏封装成组件custom-nav。组件的内部结构分三层:第一层是状态栏占位,高度等于statusBarHeight;第二层是导航栏容器,高度等于navHeight,文字和返回按钮垂直居中;第三层是内容占位,高度为0,避免后续内容被导航栏遮挡。胶囊按钮的位置由微信控制,组件需要通过计算预留出右侧空间,避免自己的按钮或者文字和胶囊重叠。
下面是一个最小可用的组件模板:
<view class="custom-nav" style="padding-top: {{statusBarHeight}}px;"> <view class="nav-bar" style="height: {{navHeight}}px;"> <view class="nav-bar__content"> <view class="nav-bar__left" bindtap="handleBack"> <image wx:if="{{showBack}}" src="/images/back.png" mode="aspectFit" /> </view> <view class="nav-bar__title">{{title}}</view> <view class="nav-bar__right"></view> </view> </view> </view>对应的样式关键点是让nav-bar__content使用flex布局,左中右三块均匀分布,并且中间标题不能被左右元素挤歪。如果左侧有返回按钮,右侧可能需要根据胶囊宽度设置等宽的占位,否则标题不会居中。在iPhone上胶囊按钮宽度大约87px,安卓机型则一般在80到95px之间,所以动态获取menuRect后,将right宽度设置为menuRect.width即可。
3.3 组件属性与数据绑定
组件的JS部分需要接收外部传入的标题、是否显示返回箭头等,并在attached生命周期中计算导航信息。下面是一个完整示例:
Component({ properties: { title: { type: String, value: '' }, showBack: { type: Boolean, value: true }, bgColor: { type: String, value: '#ffffff' }, frontColor: { type: String, value: '#000000' } }, data: { statusBarHeight: 20, navHeight: 44, menuRect: {} }, lifetimes: { attached() { const windowInfo = wx.getWindowInfo(); const menuRect = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = windowInfo.statusBarHeight; const navHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height; this.setData({ statusBarHeight, navHeight, menuRect }); if (this.properties.frontColor) { wx.setNavigationBarColor({ frontColor: this.properties.frontColor, animation: { duration: 0, timingFunc: 'easeIn' } }); } } }, methods: { handleBack() { const pages = getCurrentPages(); if (pages.length > 1) { wx.navigateBack(); } else { wx.switchTab({ url: '/pages/index/index' }); } } } });这里的attached生命周期会在组件初始化时执行,拿到的是当时窗口的尺寸。需要注意,如果页面在onLoad中通过wx.setNavigationBarColor修改状态栏颜色,可能会和组件的设置冲突。我一般会在组件中统一处理,不再在页面里重复调用。属性bgColor用来设置导航栏容器的背景色,但在组件中使用时,需要动态绑定到style上,例如style="height: {{navHeight}}px; background-color: {{bgColor}};"。
提示:组件样式建议设置
styleIsolation: 'isolated',否则页面全局样式可能会穿透进来,导致高度或间距被意外覆盖。
3.4 在页面中使用组件并验证最小效果
在页面的json中声明组件:
{ "usingComponents": { "custom-nav": "/components/custom-nav/index" } }然后在页面的wxml中直接放置:
<custom-nav title="个人中心" showBack="{{false}}" bgColor="#f5f5f5" /> <view class="page-content" style="padding-top: {{navBarHeight}}px;"> <!-- 页面内容 --> </view>注意页面的内容需要设置padding-top,值为整个导航栏的总高度(状态栏高度+导航栏高度)。但这里有个坑:页面并不直接知道组件计算出的navHeight,因此我通常把计算逻辑抽到一个公共JS文件中,或者使用behavior,让页面和组件共享同一份数据。如果只是快速验证,可以忽略padding-top,用开发者工具的模拟器切换机型,观察导航栏是否保持在正确的位置。
这时候看模拟器,不同机型的胶囊按钮位置会变化,但我们的导航栏应该始终和胶囊按钮对齐。如果没有对齐,优先检查是否使用了有效的胶囊坐标接口,其次检查样式中的padding-top是否错误地加在了custom-nav外面。
4. 完整实例:结合页面滚动和下拉效果的自定义导航栏
4.1 页面级导航栏状态管理
在实际项目中,导航栏经常需要根据页面滚动改变背景色或文字颜色。比如一个商品详情页,顶部是图片,滚动前导航栏透明,滚动后变成白色。这个效果不能只靠组件内部实现,因为滚动事件在页面层。常见做法是页面把滚动状态传给导航栏组件,组件根据状态切换样式。
下面给一个页面wxml中结合滚动事件的写法:
<custom-nav title="{{pageTitle}}" showBack="{{true}}" bgColor="{{navBgColor}}" frontColor="{{navFrontColor}}" />页面js中监听滚动:
Page({ data: { pageTitle: '商品详情', navBgColor: 'transparent', navFrontColor: '#ffffff' }, onPageScroll(e) { const scrollTop = e.scrollTop; if (scrollTop > 50) { this.setData({ navBgColor: '#ffffff', navFrontColor: '#000000' }); } else { this.setData({ navBgColor: 'transparent', navFrontColor: '#ffffff' }); } } });注意这里如果导航栏是transparent,需要同时把状态栏背景也设置为透明,否则会出现状态栏仍然是白色背景,但下面的导航栏透明了,视觉上断裂。不过实际上,自定义导航栏后状态栏背景完全由页面背景决定,所以只要页面最顶层视图中包含一个和状态栏高度相同的半透明或透明view,状态栏背景就会呈现对应效果。设置transparent时,前一个页面返回的动作可能会透出底部页面,需要额外处理,一般不推荐全透明,可以用白色带透明度来做渐变。
4.2 适配刘海屏、灵动岛和安卓挖孔屏
不同机型的顶部安全区差异很大。iOS刘海屏状态栏高度一般是44px或47px,灵动岛系列是59px;安卓常见是24px到48px。胶囊按钮的位置也会随机型变化。我们的计算方案已经用statusBarHeight和menuRect动态适配,不需要硬编码。但还有一个经常踩坑的地方:当状态栏高度非常高,比如灵动岛59px时,navHeight仍然由胶囊位置计算,组件顶部占位高度等于状态栏高度,会使整个导航栏变得更高,页面上下留白变大,这是正常现象。
安卓挖孔屏比较特殊,部分机型的状态栏上边有摄像头,微信返回的胶囊按钮会避开挖孔区域,所以只要用wx.getMenuButtonBoundingClientRect()就能跟随胶囊位置,不需要手动避开摄像头。另外有些安卓机型存在“状态栏背景颜色设置不生效”的老问题,可以通过给状态栏占位view设置纯色背景,并用!important或重新设置样式覆盖来解决。
为了直观展示适配结果,可以使用下面的参数表作为调试参考:
| 机型分类 | 典型状态栏高度(px) | 胶囊top值(px) | 计算navHeight(px) |
|---|---|---|---|
| iPhone 8 | 20 | 24 | 40 |
| iPhone 13 Pro | 47 | 51 | 40 |
| iPhone 14 Pro Max | 59 | 63 | 40 |
| 华为Mate40 Pro | 32 | 36 | 40 |
| 小米11 | 28 | 32 | 40 |
注意表中navHeight在大部分机型上接近40px,这是因为胶囊按钮的上下边距通常各为4px,高度为32px,所以4 * 2 + 32 = 40。这个规律可以用于快速验证计算是否正确。如果某个机型得到的navHeight偏差很大,说明接口返回异常或公式写错,可以打印menuRect.top和statusBarHeight来排查。不同系统版本可能调整胶囊尺寸,以上数据只做参考,具体以wx.getMenuButtonBoundingClientRect()的实际返回为准。
4.3 返回按钮、右侧视图与胶囊的间距控制
导航栏中放返回按钮时,左侧距离屏幕边缘有安全边距,右侧则要避开胶囊。我用的是左侧固定padding,右侧动态预留胶囊宽度。更精确的方法是把右侧视图宽度设为menuRect.width,并加一个margin-right: 4px来保持视觉呼吸感。但有时候胶囊按钮右侧还有一小块空位,实际生产环境中我通常直接使用menuRect.right到屏幕右侧的距离作为右侧占位宽度的一半,实现左右视觉平衡。
下面给出控制间距的样式片段:
.nav-bar__content { display: flex; align-items: center; justify-content: space-between; height: 100%; padding: 0 16px; } .nav-bar__right { width: 150px; /* 由js动态设置 */ height: 32px; display: flex; align-items: center; justify-content: flex-end; }这里的宽度不能只根据胶囊宽度设置,因为胶囊左侧可能还有滑动返回区域。在iOS上,从屏幕左缘向右滑动可以触发返回,如果我们的自定义导航栏左侧放了一个按钮,可能会遮挡手势区域。微信原生没有提供关闭这种手势的接口,只能尽量让左侧按钮不要设计得太窄,或者使用页面级enablePullDownRefresh等不会影响手势。
4.4 横竖屏切换与尺寸变化
微信小程序支持横屏后,状态栏高度和胶囊位置可能会发生变化。组件的attached只在初始化时执行一次,尺寸变化后数据是旧的。需要监听resize事件。可以在页面中注册:
wx.onWindowResize((res) => { // 重新计算并setData });但要注意,wx.onWindowResize在页面onUnload时需要移除,使用同一个回调引用才能正常offWindowResize。更好的做法是组件内部监听,但小程序组件生命周期中没有自带resize勾子,需要页面通过this.selectComponent调用组件的更新方法。在完整实例中,我会在组件中暴露一个updateNav()方法,给页面调用:
methods: { updateNav() { const windowInfo = wx.getWindowInfo(); const menuRect = wx.getMenuButtonBoundingClientRect(); // 重新计算并setData } }然后在页面onResize或onWindowResize回调里调用。
5. 把导航信息公共化:用工具函数避免重复计算并做真机巡检
到了项目后期,多个页面都需要自定义顶部导航栏,重复复制组件和计算逻辑会让状态管理变乱。我一般会把导航信息抽成一个公共模块,导出getNavigationInfo()函数,页面或组件都引用同一套逻辑。组件内部不再自己计算,而是在attached时从公共函数读取一次,同时暴露一个update方法。页面在滚动、旋转、或者从弹窗返回时,可以手动刷新。
具体做法是新建utils/nav.js:
let cache = null; function getNavigationInfo(force = false) { if (cache && !force) return cache; const windowInfo = wx.getWindowInfo(); const menuRect = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = windowInfo.statusBarHeight; const navHeight = (menuRect.top - statusBarHeight) * 2 + menuRect.height; const menuWidth = menuRect.width; const menuHeight = menuRect.height; const menuRight = menuRect.right; const screenWidth = windowInfo.screenWidth; cache = { statusBarHeight, navHeight, navBarHeight: statusBarHeight + navHeight, menuWidth, menuHeight, menuRight, contentTop: statusBarHeight + navHeight, screenWidth }; return cache; } module.exports = { getNavigationInfo };这里使用cache可以在同一页面多次调用时避免重复计算,但当页面旋转后需要传true强制刷新。这个模块没有任何页面依赖,也方便在开发者工具的console里直接执行验证。验证方法是打开页面,在console中调用getNavigationInfo(),对比打印出的statusBarHeight、navHeight和真机上的实际视觉位置。
最后一个实用技巧是:在页面onReady后,通过wx.createSelectorQuery()选择导航栏节点,获取它的boundingClientRect,和getNavigationInfo()返回的navBarHeight做对比。如果两者有超过2px的偏差,说明有样式被全局文件覆盖,应该检查组件的styleIsolation设置。组件应当设置为styleIsolation: 'isolated',避免页面外层样式穿透进入组件,导致高度计算失效。这个检查步骤虽然简单,但能省下大量真机调试时间。
每次发布前,我会拿一台iPhone、一台安卓中低端机和一台最新旗舰机跑一遍首页、详情页和个人页,观察导航栏和胶囊按钮是否在一条水平线上。自定义顶部导航栏的最终目标不是让代码看起来复杂,而是让用户在所有机型上都感觉不出你的导航栏是自定义的。做好这一步,导航栏适配才算真正收敛。
本文还有配套的精品资源,点击获取