为什么需要自定义导航栏?
原生小程序导航栏虽然开箱即用,但在实际开发中常常遇到以下痛点:
- 设计风格受限:无法自由定制背景、字体颜色、高度,难以融入品牌设计
- 扩展能力弱:无法在导航栏区域增加搜索框、自定义按钮、动态效果等
- 无法实现沉浸式:标题栏与状态栏分离感强,缺少整体感
- 屏幕适配难:不同机型(刘海屏、挖孔屏、动态岛)的高度表现不一致
因此,自定义导航栏成为许多商业项目和小程序框架的标配。本文将手把手教你实现一套适配所有主流机型、易扩展、性能优良的自定义导航栏组件。
基础配置:开启自定义模式
在 app.json 中将 navigationStyle 设置为 custom:
1 2 3 4 5 6 7 8
| { "window": { "navigationStyle": "custom", "navigationBarTextStyle": "black" // 仅对页面内文本颜色有影响,导航栏已隐藏 } }
|
💡 小贴士:也可以单独为某个页面配置,在页面目录的 .json 文件中设置相同字段即可。
配置完成后,小程序所有页面将不再渲染原生导航栏,状态栏区域完全暴露,需要我们自己实现导航UI和内容区域的避让。
核心技术:动态获取设备信息
实现完美适配的关键在于准确获取两个高度:
- 状态栏高度 (statusBarHeight)
- 胶囊按钮位置信息 (menuButtonBoundingClientRect)
获取关键数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const systemInfo = wx.getSystemInfoSync(); const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
const { statusBarHeight } = systemInfo; const { top, height } = menuButtonInfo;
const gap = top - statusBarHeight; const navContentHeight = gap * 2 + height;
const navTotalHeight = statusBarHeight + navContentHeight;
|
⚠️ 注意:getMenuButtonBoundingClientRect 必须在页面渲染完成后调用才能获取正确值,建议放在 onReady 或组件的 ready 生命周期中。
封装导航栏组件 (NavBar)
为了提高复用性,我们将导航栏封装成自定义组件。
1. 组件代码结构
1 2 3 4 5
| components/nav-bar/ ├── index.js ├── index.json ├── index.wxml └── index.wxss
|
2. index.js 核心逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| Component({ properties: { title: { type: String, value: '' }, showBack: { type: Boolean, value: true }, backgroundColor: { type: String, value: '#ffffff' }, titleColor: { type: String, value: '#000000' }, backIconColor: { type: String, value: '#000000' } },
data: { statusBarHeight: 20, navContentHeight: 44, navTotalHeight: 64, menuButtonWidth: 87 },
lifetimes: { attached() { this.initNavInfo(); } },
pageLifetimes: { show() { this.initNavInfo(); } },
methods: { initNavInfo() { try { const systemInfo = wx.getSystemInfoSync(); const menuBtn = wx.getMenuButtonBoundingClientRect(); const { statusBarHeight } = systemInfo; const { top, height, width } = menuBtn; const gap = top - statusBarHeight; const navContentHeight = gap * 2 + height; const navTotalHeight = statusBarHeight + navContentHeight; this.setData({ statusBarHeight, navContentHeight, navTotalHeight, menuButtonWidth: width }); this.triggerEvent('heightChange', { navTotalHeight }); } catch (e) { console.error('获取导航信息失败', e); this.setData({ statusBarHeight: 20, navContentHeight: 44, navTotalHeight: 64 }); } },
handleBack() { const pages = getCurrentPages(); if (pages.length === 1) { wx.switchTab({ url: '/pages/index/index' }); } else { wx.navigateBack({ delta: 1, fail: () => { wx.switchTab({ url: '/pages/index/index' }); } }); } },
handleRightTap() { this.triggerEvent('rightTap'); } } });
|
3. index.wxml 结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <view style="height: {{navTotalHeight}}px;"></view>
<view class="nav-bar-fixed" style="height: {{navTotalHeight}}px; background-color: {{backgroundColor}}; padding-top: {{statusBarHeight}}px; box-sizing: border-box;"> <view class="nav-content" style="height: {{navContentHeight}}px;"> <view class="nav-left" bindtap="handleBack" wx:if="{{showBack}}"> <icon type="default" class="back-icon" style="color: {{backIconColor}};" size="22" /> </view> <view class="nav-left" wx:else></view>
<view class="nav-title" style="color: {{titleColor}};">{{title}}</view>
<view class="nav-right" bindtap="handleRightTap"> <slot name="right"></slot> </view> </view> </view>
|
4. index.wxss 样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| .nav-bar-fixed { position: fixed; top: 0; left: 0; right: 0; z-index: 999; background-color: #ffffff; }
.nav-content { display: flex; align-items: center; justify-content: space-between; width: 100%; box-sizing: border-box; padding: 0 32rpx; }
.nav-left, .nav-right { width: 80rpx; display: flex; align-items: center; justify-content: center; }
.back-icon { width: 44rpx; height: 44rpx; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E"); background-size: contain; }
.nav-title { flex: 1; text-align: center; font-size: 34rpx; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 20rpx; }
|
注意:为了方便演示,这里用SVG DataURI 作为返回箭头。实际项目中推荐使用本地图片或iconfont字体。
5. 注册组件
在 app.json 或页面配置中注册全局组件:
1 2 3 4 5
| { "usingComponents": { "nav-bar": "/components/nav-bar/index" } }
|
页面中使用导航栏
1. 页面的 WXML
1 2 3 4 5 6 7 8
| <nav-bar title="个人中心" showBack="{{true}}" bind:heightChange="onNavHeightChange" />
<view class="page-content"> <view class="card">内容卡片</view> </view>
|
2. 页面的 JS (可选)
如果需要对页面内容额外调整,可以监听高度变化事件:
1 2 3 4 5 6 7 8 9
| Page({ data: { navBarHeight: 0 }, onNavHeightChange(e) { this.setData({ navBarHeight: e.detail.navTotalHeight }); } });
|
避坑指南与性能优化
✅ 必须避开的坑
问题现象 原因分析 解决方案
部分安卓机型胶囊信息获取为0 getMenuButtonBoundingClientRect 在 onLoad 中调用过早 延迟调用或放在 onReady / 组件的 ready 生命周期
小程序闪动/布局错位 未设置默认高度,初始化时无数据导致视图变化 先在data中预设常见机型的默认高度(如 64/44)
页面内容被导航栏遮挡 忘记留出占位区域 使用组件内占位view,或页面容器设置padding-top为导航总高
右侧胶囊会遮挡自定义返回按钮 没有给导航栏右侧预留足够的空白区域 按照胶囊宽度,在nav-right区域设置margin-right动态值(一般胶囊宽87px,可用系统信息计算后给padding) 改进方案:组件样式保证右上角无交互元素即可,默认胶囊在右侧,自定义导航栏覆盖胶囊区域不会影响点击。但为了不遮挡标题文字,标题区应留有padding-right
横屏/折叠屏适配异常 屏幕旋转后导航高度未重新计算 监听 wx.onWindowResize 重新调用获取方法(需要页面级别处理,组件内部无法直接监听,建议页面监听后更新组件数据)
🚀 性能优化建议
· 避免重复获取:在 app.js 启动时获取一次设备信息并挂载到全局,组件优先使用全局缓存。
· 减少setData频率:初始化一次性设置完整数据,不要在多个生命周期重复设置。
· 动画独立:如果需要滚动渐变效果(导航栏透明变不透明),最好单独建立一个透明导航层,避免频繁setData。
扩展进阶:动态背景与滚动渐变
有时候我们需要导航栏随着页面滚动而改变背景透明度(例如从透明逐渐变为白色)。这时需要:
- 页面监听 onPageScroll 事件
- 将滚动偏移量传递给导航栏组件
- 组件内部动态计算背景色的rgba值
由于篇幅有限,这个高级用法留作课后作业,感兴趣的读者可以尝试实现。
总结
自定义导航栏让小程序的设计和交互更加自由,而通过 wx.getMenuButtonBoundingClientRect 和动态高度计算,我们可以做到一套代码完美适配所有机型。
本文提供的组件已在实际项目中经过多轮测试,兼容以下场景:
· iOS(刘海屏、动态岛、非全面屏)
· Android(挖孔屏、水滴屏、全面屏)
· 折叠屏(展开/折叠状态需单独适配resize事件)
· iPad / 平板(状态栏高度变化)
只需复制代码稍作样式调整,即可集成到你的小程序中。