简介这份PDF资料面向微信小程序开发者尤其是电商类小程序的开发人员聚焦于自定义导航栏中“胶囊按钮”返回与首页功能的实现。当商品页被转发后页面栈仅剩一层系统默认返回按钮消失用户难以浏览其他商品或回到首页资源正是针对这一痛点给出完整解决方案。压缩包内仅含1个PDF文件大小约86KB内容围绕navigationStyle配置、状态栏与标题栏高度计算、wx.getMenuButtonBoundingClientRect与wx.getSystemInfoSync的配合使用以及headerNavbar组件的js、json、wxml、wxss四件套组织方式展开并附有项目代码地址与目录结构说明。目前已有6725人学习下载读者可从中掌握胶囊按钮位置计算、返回与首页按钮的点击逻辑、全局变量共享及下拉刷新遮挡等排错思路适合需要提升转发场景浏览体验的中级开发者参考。1. 从分享卡片进来就回不去首页这个自定义导航栏到底补了什么洞做电商类小程序的人多半遇到过这个场景商品页被用户转发到聊天里别人点卡片直接落到商品详情页面栈里只有这一条记录微信原生导航栏左侧的返回箭头直接消失。用户看完这件商品想回首页逛逛别的发现没有入口只能右上角胶囊里点「重新进入小程序」体验断得很难受。这个资源要解决的就是这件事——用navigationStyle: custom关掉原生导航栏自己画一条顶部栏在左侧补一个「返回 首页」的胶囊按钮页面栈只有一层时只显示首页多层时返回和首页都给出来。它适合正在做小程序商城、内容社区、活动页的开发者尤其是那些页面会被大量转发、又希望转发落地页能留住用户的场景。整套东西不复杂一个自定义组件加一个全局变量初始化但里面关于状态栏高度、胶囊坐标换算、下拉刷新遮挡的细节不踩一遍很难写对。下面按「原理 → 组件实现 → 页面接入 → 避坑 → 进阶」的顺序拆开讲代码可以直接抄。2. 坐标换算与全局初始化胶囊位置到底怎么算才不飘2.1 为什么不能写死 20px 和 44px很多人第一版自定义导航栏是这么写的状态栏固定 20px标题栏固定 44px加起来 64px。在 iPhone 6/7/8 上看着没问题换到 iPhone X 及以后的全面屏机型状态栏变成 44px顶部直接塌下去一截标题被刘海压住。这个资源里作者一开始也写死了高度后来改成动态计算原因就在这。微信从基础库 2.1.0 开始提供了wx.getMenuButtonBoundingClientRect()返回右侧原生胶囊按钮的边界信息坐标原点在屏幕左上角。拿到这个信息就能反推出状态栏高度、导航栏高度、以及我们自己要画的左侧胶囊该放哪。核心思路是原生胶囊在导航栏里是垂直居中的所以它上边距和下边距相等用这个对称关系就能把导航栏总高算出来。2.2 全局变量初始化app.js 里先存两份数据状态栏高度和胶囊位置信息在多个页面都要用放在app.js的globalData里最省事避免每个页面重复调用。// app.js App({ onLaunch: function (options) { // 获取设备信息里面含 statusBarHeight、screenWidth 等 wx.getSystemInfo({ success: (res) { this.globalData.systeminfo res }, }) // 获取右侧原生胶囊按钮位置信息 // 注意这个 API 没有 success 回调直接当对象用 this.globalData.headerBtnPosi wx.getMenuButtonBoundingClientRect() }, globalData: { systeminfo: {}, // 系统信息含 statusBarHeight、screenWidth headerBtnPosi: {} // 原生胶囊按钮位置信息 } })这里有个容易翻车的点wx.getMenuButtonBoundingClientRect()不像wx.getSystemInfo那样有success回调它同步返回一个对象直接.height、.top这样取就行。如果你习惯性写成wx.getMenuButtonBoundingClientRect({ success: ... })拿到的永远是空对象导航栏高度算出来是 0页面顶部一片空白。2.3 坐标换算从屏幕原点转到导航栏原点原生胶囊的坐标是以屏幕左上角为原点的我们要画的自定义胶囊是放在导航栏内部的得把原点换算到导航栏左上角。作者在组件里做了一次转换逻辑如下// headerNavbar.js 中的坐标换算 let statusBarHeight app.globalData.systeminfo.statusBarHeight // 状态栏高度 let headerPosi app.globalData.headerBtnPosi // 原生胶囊位置信息 // 以 iPhone 6 为例原生胶囊返回 // { width: 87, height: 32, top: 26, bottom: 58, right: 365 } // 状态栏高度 20屏幕宽度 375 let btnPosi { height: headerPosi.height, width: headerPosi.width, // 胶囊上边界 - 状态栏高度 胶囊距导航栏顶部的距离 top: headerPosi.top - statusBarHeight, // 胶囊下边界 - 胶囊高度 - 状态栏高度 胶囊距导航栏底部的距离 bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, // 屏幕宽度 - 胶囊右边界 胶囊距屏幕右侧的距离 right: app.globalData.systeminfo.screenWidth - headerPosi.right } // 导航栏总高 原生胶囊下边界 换算后的下边距 // 因为胶囊在导航栏里垂直居中上下边距相等 let navbarHeight headerPosi.bottom btnPosi.bottom参数说明一下top是自定义胶囊距导航栏顶部的距离bottom是距导航栏底部的距离两者应该相等垂直居中。right是自定义胶囊距屏幕右侧的距离用来把左侧胶囊定位到和右侧原生胶囊对称的位置。navbarHeight就是整个导航栏的高度等于原生胶囊下边界加上换算后的下边距。提示如果你在开发者工具里拿到的胶囊信息带小数比如{height: 24, width: 65.25, top: -0.5}八成是模拟器的视图缩放不是 100%还原成 100% 就正常了。这个坑我见过不止一个人踩。3. 组件实现wxml 布局、wxss 定位与页面栈判断3.1 wxml 结构三段式布局加手写 loading自定义导航栏组件分两部分顶部导航栏本体和底部一个手写的 loading。为什么要手写 loading因为导航栏是fixed定位的会盖住小程序原生的下拉刷新动画用户下拉时只看到一片空白体验很怪。作者的做法是在组件最底下放一个和导航栏等高的 loading 占位原生 loading 被挡住时它顶上来。!-- headerNavbar.wxml -- view classnavbar-wrap styleheight:{{navbarHeight}}px;padding-top:{{statusBarHeight}}px; view classnavbar-text styleline-height:{{navbarBtn.height navbarBtn.top}}px; {{navbarData.title ? navbarData.title : 默认标题}} /view view classnavbar-icon wx:if{{navbarData.showCapsule ? navbarData.showCapsule : true}} styletop:{{navbarBtn.top statusBarHeight}}px;left:{{navbarBtn.right}}px;height:{{navbarBtn.height}}px; image wx:if{{haveBack}} bindtap_goBack classfloatL src/img/navbar_back_white.png/image view wx:if{{haveBack}} classfloatL/view image bindtap_goHome src/img/navbar_home_white.png/image /view /view !-- 手写 loading高度与导航栏一致 -- view classnavbar-loading styleheight:{{navbarHeight}}px;line-height:{{navbarHeight}}px; text.../text /view布局上navbar-wrap用padding-top顶开状态栏高度内部标题用line-height撑到胶囊高度加顶部距离保证文字垂直居中。左侧胶囊用top和left定位top是胶囊距导航栏顶部距离加上状态栏高度left是胶囊距屏幕右侧距离——这里作者用left而不是right是因为要跟右侧原生胶囊对称用right定位反而不好算。3.2 页面栈判断什么时候显示返回按钮返回按钮的显示逻辑靠getCurrentPages()判断。页面栈长度为 1 时说明是从分享卡片直接进来的没有上一页可返回只显示首页按钮长度大于 1 时返回和首页都显示。// headerNavbar.js attached 生命周期 attached: function () { let statusBarHeight app.globalData.systeminfo.statusBarHeight let headerPosi app.globalData.headerBtnPosi let btnPosi { height: headerPosi.height, width: headerPosi.width, top: headerPosi.top - statusBarHeight, bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, right: app.globalData.systeminfo.screenWidth - headerPosi.right } let haveBack; if (getCurrentPages().length 1) { haveBack false; // 分享进入无返回 } else { haveBack true; // 正常跳转有返回 } this.setData({ haveBack: haveBack, statusBarHeight: statusBarHeight, navbarHeight: headerPosi.bottom btnPosi.bottom, navbarBtn: btnPosi }) }, methods: { _goBack: function () { wx.navigateBack({ delta: 1 }); }, _goHome: function () { wx.switchTab({ url: /pages/index/index }); } }_goBack用wx.navigateBack返回上一页_goHome用wx.switchTab跳首页。这里注意如果你的首页不是 tabBar 页面switchTab会失败得换成wx.reLaunch。这个资源里首页是 tabBar 页所以用switchTab没问题但换项目就得改。3.3 wxss 定位fixed 加高 z-index样式部分关键是fixed定位和足够高的z-index保证导航栏盖在所有内容之上。/* headerNavbar.wxss */ .navbar-wrap { position: fixed; width: 100%; top: 0; z-index: 9999999; background-color: #3281FF; box-sizing: border-box; } .navbar-text { text-align: center; font-size: 36rpx; color: #fff; font-weight: 600; } .navbar-icon { position: fixed; display: flex; border-radius: 64rpx; border: 0.5px solid rgba(255,255,255, 0.3); box-sizing: border-box; } .navbar-icon image { height: 20px; width: 20px; padding: 5px 10px 10px; display: inline-block; overflow: hidden; } .navbar-icon view { height: 18px; border-left: 0.5px solid rgba(255,255,255, 0.3); margin-top: 6px; } .navbar-loading { background: #fff; text-align: center; }.navbar-icon用border-radius: 64rpx做出胶囊圆角border用半透明白色描边和原生胶囊视觉一致。两个图标之间用一条竖线分隔就是.navbar-icon view那个border-left。图标尺寸 20px内边距上下左右调一下让点击热区够大。4. 页面接入json 配置、组件引用与下拉刷新处理4.1 页面 json 开启自定义导航在需要自定义导航栏的页面 json 里加navigationStyle: custom同时开启下拉刷新。{ navigationStyle: custom, enablePullDownRefresh: true, backgroundTextStyle: light, usingComponents: { headerNavbar: /components/headerNavbar/headerNavbar } }navigationStyle: custom关掉原生导航栏enablePullDownRefresh: true开启下拉刷新backgroundTextStyle: light把原生 loading 的三个点改成白色——这样即使它被导航栏挡住也不会露出深色圆点显得突兀。这三个配置缺一不可少一个都会在真机上看出破绽。4.2 页面 wxml 引用组件并传参!-- navigationStyle.wxml -- headerNavbar navbar-data{{nvabarData}}/headerNavbar view classhome-page text上面是自定义导航栏/text text下面是主体内容/text navigator url./testPage跳转到测试页/navigator /view组件通过navbar-data接收父页面传的对象里面控制标题和是否显示胶囊。4.3 页面 js 传参控制标题与胶囊// navigationStyle.js Page({ data: { nvabarData: { showCapsule: 1, // 1 显示左上角胶囊按钮0 不显示 title: 组件列表 // 导航栏中间标题 } }, onPullDownRefresh() { setTimeout(() { wx.stopPullDownRefresh(); // 停止下拉刷新 }, 2000); } })showCapsule控制左侧胶囊是否显示title是导航栏标题。下拉刷新里用setTimeout模拟异步请求2 秒后停止刷新。实际项目里换成真实接口回调即可。4.4 主体内容防遮挡占位 box 的处理导航栏是fixed的会盖住主体内容顶部所以要在导航栏和主体之间加一个等高的占位元素。作者在组件里已经用navbar-loading做了这个占位高度和导航栏一致。但这里有个细节如果页面支持下拉刷新原生 loading 会被导航栏挡住占位 box 在刷新结束后才收上去用户会看到莫名多出一块空白。所以组件底部那个手写 loading 就是用来顶替原生 loading 的刷新时它显示刷新结束它收起视觉上连贯。5. 避坑排查真机高度偏差、坐标异常与兼容性5.1 真机高度和开发者工具不一致现象开发者工具里算出来导航栏高度 64px真机 iPhone 8 Plus 上量出来是 60px差了几像素。原因开发者工具的模拟器渲染和真机有细微差异wx.getMenuButtonBoundingClientRect()返回的值在不同设备上本身就有浮动加上工具缩放、系统版本差异几像素偏差很难完全消除。解决如果只是个别页面用自定义导航几像素偏差用户基本看不出来可以接受。如果是全局使用建议统一用一套计算逻辑不要混用写死高度和动态计算否则页面之间高度不一致反而更明显。5.2 胶囊坐标出现负值或小数现象wx.getMenuButtonBoundingClientRect()返回{height: 24, width: 65.25, top: -0.5, bottom: -0.5, right: 101.25}top是负数。原因开发者工具的视图缩放不是 100%模拟器按缩放比例返回了错误坐标。解决把开发者工具右下角的缩放还原成 100%重新编译坐标就正常了。这个不是代码问题是工具设置问题别去改代码。5.3 低版本微信不支持胶囊 API现象在低版本微信上wx.getMenuButtonBoundingClientRect()返回空对象或报错导航栏高度算出来是 0。原因这个 API 从基础库 2.1.0 和微信 7.0.0 才开始支持低版本没有。解决做兼容判断拿不到胶囊信息时用写死的高度兜底。常见做法是按机型给固定值iPhone 系列 64pxiPhone X 及以后 88px安卓 68px。虽然不够精确但低版本用户占比已经很低兜底够用。// 兼容低版本拿不到胶囊信息时用固定高度 let navbarHeight; if (headerPosi headerPosi.height) { navbarHeight headerPosi.bottom btnPosi.bottom; } else { // 兜底高度按机型粗略区分 const system app.globalData.systeminfo; if (system.model.indexOf(iPhone X) -1) { navbarHeight 88; } else if (system.platform android) { navbarHeight 68; } else { navbarHeight 64; } }5.4 switchTab 跳首页失败现象点首页按钮没反应控制台报switchTab:fail can not switch to no-tabBar page。原因wx.switchTab只能跳 tabBar 页面如果首页不是 tabBar 页就会失败。解决判断首页类型不是 tabBar 页就用wx.reLaunch或wx.redirectTo。这个资源里首页是 tabBar 页所以没问题但换项目一定要检查。5.5 下拉刷新时导航栏闪烁现象下拉刷新时导航栏背景色和页面背景色之间闪一下白。原因原生 loading 被导航栏挡住后页面顶部露出背景色和导航栏颜色不一致。解决把backgroundTextStyle设成light让原生 loading 变白同时确保手写 loading 的背景色和页面背景一致。如果还闪检查导航栏的background-color是否和页面顶部背景色统一。6. 进阶技巧把导航栏做成可配置的通用组件这套代码跑通之后我一般会把它改造成更通用的组件而不是每个页面复制一遍。具体做法是把标题、是否显示返回、是否显示首页、背景色、文字颜色都做成properties页面通过传参控制。这样商品页、活动页、内容页可以共用同一个组件只改配置不改代码。// 通用化改造把可变项抽成 properties Component({ properties: { navbarData: { type: Object, value: { title: 默认标题, showCapsule: true, showBack: true, showHome: true, bgColor: #3281FF, textColor: #fff } } }, // ... 其余逻辑不变 })验证方法很简单在开发者工具里切换不同机型iPhone 6、iPhone X、安卓看导航栏高度和胶囊位置是否对齐再用真机预览从分享卡片进入和从页面跳转进入各测一次确认返回按钮的显示逻辑正确。我习惯在attached里打一行console.log把navbarHeight和btnPosi打出来真机调试时对着看比盲猜快得多。还有一个细节如果你的小程序有多个 tabBar 页面首页按钮的switchTab路径要写对别写死/pages/index/index最好从配置里读。另外导航栏的z-index给到 9999999 是为了盖住所有内容但如果页面里有弹窗、遮罩要注意层级冲突弹窗的z-index得比导航栏更高。从那以后我每次做自定义导航栏都强制在真机上把 iPhone 6、iPhone X、安卓三档各跑一遍确认高度和胶囊位置对齐再提交。这个习惯帮我省了不少返工。希望帮到你。本文还有配套的精品资源点击获取