ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序自定义导航栏完全指南:状态栏适配与组件化实践

微信小程序自定义导航栏完全指南:状态栏适配与组件化实践 做小程序开发这几年被导航栏折磨的次数还真不少。微信小程序默认的头部导航栏样式固定、背景色单一想放个返回键以外的功能键基本没戏更别提做沉浸式头图或者品牌色导航了。后来我干脆用navigationStyle把默认导航栏干掉自己封装了一套自定义头部导航栏组件左上角返回键、右侧功能键、中间标题区全部自己做这才算真正把页面头部的控制权握在手里。这篇文章就把这套方案的完整思路、核心代码和踩坑记录整理出来给同样被导航栏限制住的开发者一个可以直接抄作业的参考。1. 为什么我建议你自定义头部导航栏先说清楚一个事实微信默认的导航栏并不是不能做只是它的可定制空间小得可怜。你只能在app.json或页面 json 里改改navigationBarBackgroundColor、navigationBarTextStyle、navigationBarTitleText这三板斧。背景色是纯色文字只能黑或白右边最多加一个“胶囊”里的转发菜单左边除了返回键什么都放不了。1.1 默认导航栏的局限性如果你只是做一个工具类小程序页面结构简单默认导航栏完全够用。但一旦涉及以下需求默认方案就会非常吃力导航栏需要和页面头部视觉融为一体比如一张 banner 图从顶部延伸到状态栏下面形成沉浸式头图效果。导航栏背景需要跟随页面滚动渐变比如商城首页从透明渐变到品牌色。左侧除了返回键还需要显示自定义图标、文字或者业务标识。右侧需要放“首页”“分享”“更多”等自定义功能键而不仅仅是微信胶囊里的转发。不同页面需要不同风格的导航栏但全局配置只能统一设置页面级配置也只是换颜色和标题而已。这些需求如果用默认导航栏硬做只能靠“页面内布局 隐藏原生导航栏”的思路也就是我们常说的自定义导航栏。实际上微信官方也提供了解法把navigationStyle设为custom原生导航栏就会被隐藏页面内容直接延伸到屏幕顶部头部区域完全由你掌控。1.2 自定义导航栏适合哪些场景根据我的实际经验下面这几类项目最适合上自定义导航栏品牌感强的页面电商小程序首页、品牌活动页、积分商城等需要导航栏和页面主视觉统一。直播/短视频类页面头部需要叠加主播信息、关注按钮、关闭按钮直播期间还需要隐藏部分元素。需要多入口操作的页面右上角放“首页”、“客服”、“分享”等按钮左侧放返回或关闭。沉浸式阅读/展示页文章详情、图片预览、视频详情等希望顶部尽量少干扰元素。反过来如果你只是做一些后台管理、表单填写类的工具页面自定义导航栏属于“杀鸡用牛刀”还要额外处理状态栏适配反而增加工作量。判断标准很简单默认导航栏能不能满足页面视觉和交互需求不能满足再考虑自定义。1.3 自定义导航栏的基本原理自定义导航栏的核心就一句话把原生导航栏隐藏自己在页面顶部画一个导航栏组件。这个组件需要做三件事获取状态栏高度手机顶部显示时间、电量的那一条。获取右上角胶囊按钮微信菜单按钮的位置。根据胶囊位置计算自定义导航栏的高度并让返回键、标题、功能键对齐。微信的胶囊按钮是隐藏不掉的它悬浮在页面右上角你的自定义导航栏必须避开它否则按钮会重叠。这是所有自定义导航栏方案都必须考虑的基础约束。2. navigationStyle 配置的正确打开方式先从配置层说起。navigationStyle是微信小程序提供的窗口配置项取值只有default和custom两种。default是默认值就是原生导航栏custom表示自定义导航栏原生导航栏会被隐藏。2.1 全局配置与页面级配置如果你整个项目所有页面都要自定义导航栏可以在app.json的window节点里统一配置{ window: { navigationStyle: custom } }这样所有页面都会隐藏原生导航栏。如果只是个别页面需要自定义那就不要动全局配置而是在对应页面的 json 文件里单独设置{ navigationStyle: custom }这里有个优先级要注意页面级配置会覆盖全局配置。也就是说你可以全局开启custom然后在个别页面设置default恢复原生导航栏反过来也是一样。这个特性在混合项目里非常实用比如大部分页面走自定义导航栏但某个简单的协议页直接用原生导航栏省事儿。2.2 配置 custom 后发生的变化把navigationStyle设为custom之后最直观的变化是默认导航栏消失页面内容从屏幕顶部开始渲染。注意这里说的是从屏幕最顶部、也就是状态栏上面开始渲染而不是从状态栏下面。这意味着如果不做任何处理页面内容会直接顶着状态栏时间、电量、信号图标会被页面内容遮挡。所以自定义导航栏组件的第一个任务就是预留出状态栏高度。另外要留意一点navigationStyle是静态配置不能在页面运行过程中动态切换。比如你不能在某个按钮点击后把页面导航栏从自定义切换到原生。所有配置必须在页面加载前确定这也是为什么大多说自定义导航栏方案都是基于组件的静态渲染。2.3 一个配置引发的连锁适配配置好custom之后真正的麻烦才开始。多个平台之间需要做适配主要包括不同机型的状态栏高度不同iPhone 的刘海屏和非刘海屏差异很大Android 阵营更乱。胶囊按钮的位置在不同机型上也有差异需要通过 API 动态获取。状态栏文字颜色也要单独设置因为原生导航栏里navigationBarTextStyle已经失效了你需要在页面 json 里配置window的navigationStyle为custom后额外通过wx.setNavigationBarColor或页面配置控制状态栏前景色。这里补充一点自定义导航栏后状态栏前景色也就是文字和图标的颜色依然可以通过wx.setNavigationBarColor来设置只是第一个参数frontColor只支持#ffffff和#000000两种值没有更多选择。如果你想在浅色背景上用深色文字、深色背景上用浅色文字需要根据页面背景动态调用这个接口。3. 核心机制状态栏高度与胶囊按钮定位自定义导航栏组件能不能做得好关键就看你对两个数据的处理水平状态栏高度statusBarHeight和胶囊按钮位置menuButtonInfo。3.1 获取状态栏高度和胶囊信息在微信小程序里获取状态栏高度有两个常用 APIwx.getSystemInfoSync()老牌接口返回statusBarHeight。wx.getWindowInfo()基础库 2.20.1 之后推荐的替代接口。说实话wx.getSystemInfoSync用了很多年稳定性没问题但微信官方一直在推动开发者迁移到新版接口因为新接口的性能更好、数据更规范。我这里直接推荐用wx.getWindowInfo获取状态栏高度用wx.getMenuButtonBoundingClientRect获取胶囊信息。const windowInfo wx.getWindowInfo(); const menuButtonInfo wx.getMenuButtonBoundingClientRect(); const statusBarHeight windowInfo.statusBarHeight; const menuTop menuButtonInfo.top; const menuHeight menuButtonInfo.height;拿到这两组数据之后导航栏的设计就有依据了。3.2 导航栏高度与内容区偏移量的计算自定义导航栏高度怎么定通常的做法是以胶囊按钮为参考。胶囊按钮的垂直中心点基本上就是导航栏内容区的垂直中心点。因为这个中心点要和左侧的返回键、右侧的功能键对齐整体视觉才协调。计算公式如下导航栏总高度 状态栏高度 导航栏内容区高度 导航栏内容区高度 胶囊按钮高度 (胶囊按钮顶部到状态栏底部的距离) * 2胶囊按钮顶部到状态栏底部的距离就是胶囊按钮和状态栏之间的垂直间距用menuButtonInfo.top - statusBarHeight计算。这个间距在 iPhone 上大约是 12px 左右在 Android 上大约是 8px~12px 不等。用这个间距乘以 2再加上胶囊高度就是导航栏内容区的推荐高度通常是 44px。我把这个计算过程封装成一个小函数便于复用function getNavigationBarInfo() { const windowInfo wx.getWindowInfo(); const menuButtonInfo wx.getMenuButtonBoundingClientRect(); const statusBarHeight windowInfo.statusBarHeight; const menuTop menuButtonInfo.top; const menuHeight menuButtonInfo.height; const navContentHeight menuHeight (menuTop - statusBarHeight) * 2; const navTotalHeight statusBarHeight navContentHeight; return { statusBarHeight, navContentHeight, navTotalHeight, menuButtonInfo }; }3.3 为什么不能直接写死高度很多初学者会问能不能直接把导航栏高度写死成 64px答案是不能。iPhone 12 Pro 的状态栏高度是 44pxiPhone SE 2 的状态栏高度是 20pxAndroid 机型从 20px 到 48px 都有。你要是写死在部分机型上就会出现返回键和胶囊按钮不水平、标题上下偏移的问题。我在实际开发中还遇到过一种情况某些 Android 机型的窗口信息在页面刚加载时获取到的值不够准确偶尔会拿到一个偏小或偏大的状态栏高度。解决办法是在组件初始化后延迟一段时间重新获取一次或者监听wx.onWindowResize回调更新数据。不过绝大多数场景下首次同步获取就够了延迟获取属于保险措施。3.4 CSS 变量与动态样式绑定拿到导航栏高度数据之后就是把它应用到组件样式里。组件的 WXML 结构大致是这样view classnav styleheight: {{navTotalHeight}}px; padding-top: {{statusBarHeight}}px; view classnav-content styleheight: {{navContentHeight}}px; !-- 左侧返回键 -- view classnav-left bindtaphandleBack slot nameleft/slot /view !-- 中间标题 -- view classnav-title{{title}}/view !-- 右侧功能键 -- view classnav-right bindtaphandleAction slot nameright/slot /view /view /view这里的核心思路是外层 view 负责撑起整个导航栏的高度内层 nav-content 负责承载内容左右两侧按钮通过插槽对外暴露方便不同页面传入不同的按钮内容。4. 返回键和功能键组件的完整实现现实中导航栏组件很少只是一个纯展示组件它必须和业务交互绑定。这一节我完整走一遍返回键和功能键的实现过程包括组件结构、属性设计、事件通信和页面接入。4.1 组件属性和插槽设计我开发的这个导航栏组件叫custom-nav它对外暴露以下属性属性名类型默认值说明titleString导航栏标题空则显示左侧插槽内容showBackBooleantrue是否显示返回键showHomeBooleanfalse是否显示首页键无上级页面时返回首页backIconString自定义返回键图标地址默认使用内置返回箭头backgroundStringtransparent导航栏背景色colorString#000000标题文字颜色fixedBooleantrue是否固定在页面顶部组件的事件设计上返回键点击事件由组件内部处理因为返回逻辑基本都是统一的。功能键点击事件则通过triggerEvent抛给页面来处理因为不同页面的功能键语义完全不同。另外我额外设计了一个back事件让页面可以拦截返回逻辑比如在表单页提示用户“内容未保存”再返回。组件完整代码我放在下面方便大家直接参考。组件的 JS 逻辑Component({ options: { multipleSlots: true }, properties: { title: { type: String, value: }, showBack: { type: Boolean, value: true }, showHome: { type: Boolean, value: false }, backIcon: { type: String, value: }, background: { type: String, value: transparent }, color: { type: String, value: #000000 }, fixed: { type: Boolean, value: true } }, data: { statusBarHeight: 20, navContentHeight: 44, navTotalHeight: 64 }, lifetimes: { attached() { this.initNavInfo(); } }, methods: { initNavInfo() { const windowInfo wx.getWindowInfo(); const menuButtonInfo wx.getMenuButtonBoundingClientRect(); const statusBarHeight windowInfo.statusBarHeight; const navContentHeight menuButtonInfo.height (menuButtonInfo.top - statusBarHeight) * 2; this.setData({ statusBarHeight, navContentHeight, navTotalHeight: statusBarHeight navContentHeight }); }, handleBack() { const pages getCurrentPages(); if (pages.length 1) { wx.navigateBack(); } else { this.triggerEvent(back); } }, handleAction() { this.triggerEvent(action); } } });返回逻辑这里有一个关键判断页面栈长度大于 1 时直接wx.navigateBack()等于 1 时说明当前页面是入口页没有上级页面这时会触发back事件由页面自己决定是返回首页、关闭小程序还是做其他处理。另外还可以加一个逻辑如果showHome为 true 且页面栈长度等于 1可以跳转到首页。4.2 页面接入和样式适配页面接入组件很简单在页面的 json 文件里声明组件路径{ usingComponents: { custom-nav: /components/custom-nav/custom-nav } }然后在页面的 WXML 里使用custom-nav title商品详情 show-back{{true}} show-home{{false}} background#ffffff color#333333 bind:actionhandleMore /页面接收功能键点击事件Page({ handleMore() { wx.showActionSheet({ itemList: [分享, 举报], success(res) { // 处理对应操作 } }); } });这里有一个很容易被忽略的问题自定义导航栏组件的层级。组件默认是普通文档流布局如果页面内容往上滚动内容会盖住导航栏。通常我会在组件里加position: fixed并且把z-index设为一个较大值比如 999确保导航栏始终悬浮在页面内容之上。对应 WXSS 的关键片段.nav { position: fixed; top: 0; left: 0; right: 0; z-index: 999; background: transparent; box-sizing: border-box; } .nav-content { position: relative; width: 100%; display: flex; align-items: center; justify-content: space-between; padding: 0 16rpx; box-sizing: border-box; } .nav-left, .nav-right { min-width: 88rpx; height: 100%; display: flex; align-items: center; justify-content: flex-start; box-sizing: border-box; } .nav-right { justify-content: flex-end; }4.3 返回键的交互细节和边界情况返回键的逻辑听起来简单但细节很多。比如微信小程序页面栈最多只能叠加 10 层超过之后wx.navigateTo会失败。在自定义导航栏里返回键判断的同样是页面栈。但如果用户是从分享链接直接打开的小程序页面栈初始长度是 1此时点击返回键只有两个选择要么触发back事件让页面自行处理要么直接wx.reLaunch到首页。我通常的做法是组件监听返回键点击判断页面栈长度。const pages getCurrentPages(); if (pages.length 1) { wx.navigateBack(); } else if (this.data.showHome) { wx.reLaunch({ url: /pages/index/index }); } else { this.triggerEvent(back); }此外还需要注意当页面是通过wx.redirectTo跳转而来时虽然页面栈长度为 1但上一页面已经被销毁此时返回键不应该显示。这种情况下需要页面自行判断在接入组件时把showBack设为 false或者显示首页键代替。5. 兼容性差异与实战问题排查自定义导航栏方案最大的坑不在逻辑复杂而在机型适配。同一个页面iPhone 14 Pro 和 Android 中低端机的表现可能完全不一样。这里整理了我这些年遇到的高频问题。5.1 不同平台的差异表现问题现象涉及平台原因分析解决方案导航栏高度偏小按钮顶到状态栏Android 部分机型状态栏高度获取异常或含有虚拟按键区域高度使用wx.getWindowInfo()重新获取延迟 100ms 后再次计算状态栏文字和图标颜色不生效iOSwx.setNavigationBarColor在自定义导航栏下偶发失效在页面onShow中重新调用或改用页面配置navigationBarTextStyle返回键点击触发两次所有平台事件冒泡导致点击子元素同时触发父元素事件在按钮容器上用catchtap替代bindtap胶囊按钮遮住右侧功能键所有平台功能键布局未避开胶囊右侧功能区预留足够宽度建议胶囊左侧留 8px 以上间距导航栏跳动Android 部分机型状态栏高度首次获取为 0组件初始化时从本地缓存读取旧值再异步刷新新值5.2 状态栏前景色设置的稳定方案自定义导航栏之后潜台词就是状态栏区域需要你自己控制。要让状态栏文字变成白色或黑色稳定做法如下在页面的onLoad或组件attached生命周期里调用wx.setNavigationBarColor({ frontColor: #000000, backgroundColor: #ffffff });其中frontColor必填只支持#ffffff和#000000两种值。注意这个 API 虽然叫setNavigationBarColor但在自定义导航栏模式下它只影响状态栏文字颜色不会影响你自绘的导航栏背景。所以你需要和组件里的background属性配合使用。5.3 内容区被导航栏遮挡怎么处理自定义导航栏是悬浮在页面顶部的页面内容默认会从屏幕最顶部开始渲染因此如果不处理页面第一个元素会被导航栏盖住。解决方案一般有两种第一种整个页面设置 padding-top值为导航栏总高度。优势是简单直接劣势是每个页面都要算一遍。第二种给导航栏底部留一个占位元素页面内容在正常文档流中排在占位元素后面。更优雅也更容易维护。我推荐第二种方案具体做法是在组件内部底部渲染一个占位元素view classnav-placeholder styleheight: {{navTotalHeight}}px;/view然后页面使用组件时只要保证组件是页面的第一个元素内容就不会被遮挡。但这个占位元素只在position: fixed模式下需要如果组件不固定定位就不需要占位。5.4 自定义导航栏和滚动容器如果你的页面使用了scroll-view进行局部滚动需要注意自定义导航栏是fixed定位的它不会随着scroll-view滚动这是设计预期内的。但如果你需要在滚动过程中动态改变导航栏背景色比如滚动超过某个距离后从透明变成白色可以通过监听scroll-view的bindscroll事件来实现。onPageScroll(e) { const scrollTop e.scrollTop; const opacity Math.min(scrollTop / 100, 1); this.setData({ navBackground: rgba(255, 255, 255, ${opacity}) }); }需要提醒的是onPageScroll只在页面级滚动时触发如果用scroll-view就监听它的bindscroll。两者的触发时机和性能表现略有差异页面级滚动更稳定推荐优先使用。6. 自定义导航栏的进阶玩法与经验总结到这里一个基础的自定义导航栏组件已经可以正常工作了。接下来聊一些实际项目中的进阶需求以及我在这些需求里沉淀下来的经验。6.1 导航栏滚动渐变和沉浸式效果品牌页面非常流行“顶部透明导航栏 头图沉浸式效果”也就是初始状态导航栏完全透明头图从屏幕顶部开始展示文字或 logo滚动后导航栏逐渐加上背景色和毛玻璃效果。实现方法不复杂初始状态组件background设为transparent文字颜色设为白色。监听onPageScroll计算滚动距离。根据滚动距离动态设置背景色透明度滚动超过头图高度后完全变为不透明。这里有一个细节导航栏文字颜色在透明背景和实色背景下的可读性完全不同。通常的做法是在透明度超过 0.5 时统一把文字颜色切回深色低于 0.5 时保持白色。切换建议使用 CSS 渐变或 transition 过渡避免生硬跳变。6.2 多个功能键的排布策略有时候一个功能键不够用比如右侧要放“分享”和“更多”两个按钮。这时需要横向排布多个图标但右侧空间有限因为要避开胶囊按钮。胶囊按钮的宽度大概是 87px占据页面右上角一大块空间。所以在胶囊左边我们能利用的空间实际很窄通常只够放 1~2 个 40px 左右的图标。如果功能键数量超过 2 个建议收起成一个“更多”按钮点击后弹出wx.showActionSheet操作菜单来选择具体功能这是很多成熟小程序的标准做法。6.3 针对刘海屏和灵动岛的适配iPhone 14 Pro 的灵动岛区域和早期的刘海屏状态栏高度不同胶囊按钮的位置也会相应变化。好消息是微信的wx.getMenuButtonBoundingClientRect会返回真实值所以只要你的组件严格基于这个 API 动态计算高度大部分机型都能自动适配。真正容易出问题的是 Android 的挖孔屏不同厂商的挖孔位置不同状态栏高度也各种奇葩。我的建议是不要过度设计以胶囊按钮为锚点计算导航栏高度就是最通用的方案。6.4 组件化封装与多业务复用我在项目中把这个导航栏组件做成了独立的 npm 包同时支持多个小程序复用。组件对外暴露了标题、背景色、返回键、功能键插槽等属性页面不需要关心导航栏高度计算逻辑只需要传入参数即可。这里最大的收益是新页面接入成本低一行配置、一行标签就能完成。统一改版方便全局换图标、换标题样式只需要改组件内部。减少排查成本所有状态栏适配逻辑集中在一处问题定位更快。6.5 我踩过的最深的几个坑最后分享几个印象最深的实战教训。第一个坑是获取胶囊信息时在页面onLoad里同步调用返回了全 0 数据。后来排查发现是组件初始化时机太早页面配置还没完全生效。解决方案是放到组件attached生命周期里处理或者用setTimeout延迟 100ms 再获取。第二个坑是返回键事件冒泡。我在自定义导航栏的按钮区域用了bindtap结果在部分 Android 机器上出现了点击返回键同时触发下方页面元素点击事件的问题。排查后确认是事件冒泡导致把所有交互按钮的bindtap改成catchtap才解决。第三个坑是功能键被胶囊按钮遮挡。我一开始只预留了 80px 的右侧空间结果在部分 Android 机型上胶囊按钮的宽度超过了这个值导致功能键被盖住。后来我改成动态计算胶囊按钮的左侧坐标并将右侧功能键区域宽度设为从屏幕右边缘到胶囊按钮左侧的间距这个问题才彻底解决。如果你正在做自定义导航栏我强烈建议你从wx.getWindowInfo()和wx.getMenuButtonBoundingClientRect()这两个 API 入手先把高度和位置算准了再谈样式和交互。组件化封装是必须的千万别在页面里复制粘贴导航栏的代码否则后患无穷。希望这篇分享能帮你少走一些弯路也欢迎在实践中继续补充更多边界场景的处理经验。
返回列表