面向初学者:从状态栏、导航栏、胶囊按钮开始,理解微信小程序自定义顶部区域的计算逻辑,并完成一个可复用的顶部导航组件。
一、为什么“顶部区域”容易算错?
在微信小程序中,页面顶部通常不是一个简单的 44px 容器,而是由多个区域叠加而成:
屏幕顶部
┌──────────────────────────────┐
│ 状态栏:时间、电量、信号 │ statusBarHeight
├──────────────────────────────┤
│ 导航栏:标题 + 胶囊按钮 │ navBarHeight
├──────────────────────────────┤
│ 页面内容 │
└──────────────────────────────┘
常见问题包括:
- 自定义标题与右上角胶囊按钮上下不对齐;
- iPhone 刘海屏和普通 Android 手机显示效果不一致;
- 在 H5、App、微信小程序之间切换时,顶部出现重复留白;
- 使用固定
44px、64px后,某些机型出现遮挡或空白; getMenuButtonBoundingClientRect()返回空值,导致页面高度变成0。
解决这些问题的关键,不是背一个固定高度,而是先分清楚:
顶部总高度 = 状态栏高度 + 自定义导航栏高度
其中,状态栏高度可以通过窗口信息获取;微信小程序的导航栏高度则可以根据右上角胶囊按钮的位置和尺寸推算。
二、先理解三个重要概念
1. 状态栏高度 statusBarHeight
状态栏是手机系统区域,包含时间、网络、电量等信息。它不属于我们编写的页面内容。
在 uni-app 中,推荐优先使用:
const windowInfo = uni.getWindowInfo()
console.log(windowInfo.statusBarHeight)
uni.getWindowInfo() 可以返回窗口宽高、状态栏高度、安全区域等信息。对于较老的项目,也可以暂时使用 uni.getSystemInfoSync() 作为兼容方案。
2. 微信导航栏高度 navBarHeight
导航栏是状态栏下方的标题区域,一般放置:
- 页面标题;
- 返回按钮;
- 搜索框;
- 右上角胶囊按钮附近的自定义内容。
微信没有直接返回“自定义导航栏高度”的统一字段,但可以通过胶囊按钮的布局信息计算。
3. 胶囊按钮信息
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
常用字段如下:
| 字段 | 含义 |
|---|---|
top | 胶囊按钮距离屏幕顶部的距离 |
bottom | 胶囊按钮距离屏幕顶部的底部坐标 |
height | 胶囊按钮高度 |
left | 胶囊按钮左侧坐标 |
right | 胶囊按钮右侧坐标 |
width | 胶囊按钮宽度 |
这些坐标都以屏幕左上角为原点,单位通常是逻辑像素。
三、核心计算公式
公式一:根据胶囊上下边界计算导航栏高度
推荐使用下面的公式:
navBarHeight = menuButtonInfo.bottom
+ menuButtonInfo.top
- statusBarHeight
为什么这样计算?
menuButtonInfo.bottom - statusBarHeight:胶囊底部相对于导航栏顶部的位置;menuButtonInfo.top - statusBarHeight:胶囊顶部相对于导航栏顶部的位置;- 两者相加后,得到包含上下留白的导航栏高度。
公式二:根据胶囊高度和上方间距计算
也可以写成:
const menuMarginTop = menuButtonInfo.top - statusBarHeight
const navBarHeight = menuButtonInfo.height + menuMarginTop * 2
这种写法更直观,但它默认胶囊在导航栏中垂直居中。实际项目中,公式一直接使用 top 和 bottom,通常更容易表达布局关系。
顶部总高度
const topAreaHeight = statusBarHeight + navBarHeight
如果页面使用了自定义导航栏,页面内容应从 topAreaHeight 之后开始布局。
四、最小可运行示例
下面是一个适合新手理解的 Vue 3 + <script setup> 示例。
1. 页面配置
如果要自己绘制导航栏,需要关闭微信原生导航栏:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom"
}
}
]
}
建议只给确实需要自定义顶部的页面配置 navigationStyle: "custom",不要一开始就全局关闭原生导航栏。
2. 页面代码
<template>
<view class="page">
<!-- 占位:避开系统状态栏 -->
<view
class="status-bar-placeholder"
:style="{ height: `${statusBarHeight}px` }"
/>
<!-- 自定义导航栏 -->
<view
class="custom-navbar"
:style="{ height: `${navBarHeight}px` }"
>
<view class="navbar-title">首页</view>
</view>
<!-- 页面主体 -->
<scroll-view scroll-y class="page-content">
<view class="card">这里是页面内容</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const statusBarHeight = ref(0)
const navBarHeight = ref(44)
function calculateTopArea() {
const windowInfo = typeof uni.getWindowInfo === 'function'
? uni.getWindowInfo()
: uni.getSystemInfoSync()
statusBarHeight.value = windowInfo.statusBarHeight || 0
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
if (menuButtonInfo && menuButtonInfo.height > 0) {
navBarHeight.value =
menuButtonInfo.bottom +
menuButtonInfo.top -
statusBarHeight.value
}
} catch (error) {
// 胶囊信息获取失败时,使用常见导航栏高度兜底
navBarHeight.value = 44
console.warn('获取微信胶囊按钮信息失败:', error)
}
// #endif
// #ifndef MP-WEIXIN
// H5、App 等端没有微信胶囊按钮,使用项目约定的导航栏高度
navBarHeight.value = 44
// #endif
}
onMounted(() => {
calculateTopArea()
})
</script>
<style>
.page {
min-height: 100vh;
background: #f7f8fa;
}
.status-bar-placeholder {
width: 100%;
}
.custom-navbar {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 0 32rpx;
background: #ffffff;
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #222222;
}
.page-content {
height: calc(100vh - 120px);
box-sizing: border-box;
padding: 24rpx;
}
.card {
padding: 32rpx;
border-radius: 20rpx;
background: #ffffff;
}
</style>
五、推荐封装成 useTopBar 工具
页面多了以后,不要在每个页面重复写计算逻辑。可以封装成组合式函数。
新建文件:composables/useTopBar.js
import { ref } from 'vue'
export function useTopBar() {
const statusBarHeight = ref(0)
const navBarHeight = ref(44)
const topAreaHeight = ref(44)
const menuButtonInfo = ref(null)
function calculate() {
const windowInfo = typeof uni.getWindowInfo === 'function'
? uni.getWindowInfo()
: uni.getSystemInfoSync()
statusBarHeight.value = windowInfo.statusBarHeight || 0
// #ifdef MP-WEIXIN
try {
const menuInfo = uni.getMenuButtonBoundingClientRect()
if (menuInfo && menuInfo.height > 0) {
menuButtonInfo.value = menuInfo
navBarHeight.value =
menuInfo.bottom + menuInfo.top - statusBarHeight.value
} else {
navBarHeight.value = 44
}
} catch (error) {
navBarHeight.value = 44
}
// #endif
// #ifndef MP-WEIXIN
navBarHeight.value = 44
// #endif
topAreaHeight.value = statusBarHeight.value + navBarHeight.value
}
return {
statusBarHeight,
navBarHeight,
topAreaHeight,
menuButtonInfo,
calculate
}
}
页面中使用:
<script setup>
import { onMounted } from 'vue'
import { useTopBar } from '@/composables/useTopBar'
const {
statusBarHeight,
navBarHeight,
topAreaHeight,
menuButtonInfo,
calculate
} = useTopBar()
onMounted(() => {
calculate()
})
</script>
六、如何让标题与胶囊按钮真正对齐?
仅仅算出导航栏高度还不够。如果标题或搜索框需要放在胶囊按钮左侧,还需要使用胶囊的横坐标。
例如:
const rightSafeSpace = windowInfo.windowWidth - menuButtonInfo.right
const titleRight = menuButtonInfo.left
模板中可以这样设置:
<view
class="navbar-title"
:style="{
marginRight: `${windowWidth - menuButtonInfo.left}px`
}"
>
页面标题
</view>
更常见的做法是让导航栏使用左右内边距,并把右侧操作区域的宽度设置为胶囊按钮宽度加间距:
<view class="navbar-inner">
<view class="navbar-left">返回</view>
<view class="navbar-title">订单详情</view>
<view
class="navbar-right-placeholder"
:style="{
width: `${menuButtonInfo.width + 16}px`
}"
/>
</view>
这样标题区域不会侵入右上角胶囊按钮的空间。
七、onLoad、onShow 还是 onMounted?
适合的调用时机
- Vue 3 页面:可以在
onMounted中计算; - uni-app 页面生命周期:可以在
onLoad或onShow中计算; - 如果页面结构或窗口状态会变化,可以在
onShow中重新计算一次。
不建议的调用时机
不要在模块顶层直接执行:
// 不推荐
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
原因是此时页面可能还没有进入小程序页面生命周期,某些环境下可能拿到空对象、默认值或触发异常。
八、CSS 变量能不能直接使用?
uni-app 提供了 --status-bar-height、--window-top、--window-bottom 等 CSS 变量,可以用于简单布局。例如:
.page {
padding-top: var(--status-bar-height);
}
不过,对于微信小程序的精确自定义导航栏,不建议只依赖 CSS 变量。原因是:
- 变量适合表达平台统一的布局约定;
- 自定义导航栏还需要知道胶囊按钮的位置和大小;
- 不同端的 CSS 变量语义不完全相同;
- 复杂导航布局通常需要 JavaScript 得到真实的像素值。
因此可以采用下面的策略:
- 状态栏占位:简单页面可以用 CSS 变量;
- 精确导航栏:用
uni.getWindowInfo()+ 胶囊信息计算; - 跨端项目:通过条件编译分别处理微信小程序、H5 和 App。
九、常见错误与排查方式
错误 1:把顶部总高度误当成导航栏高度
错误写法:
contentTop = navBarHeight
正确理解:
contentTop = statusBarHeight + navBarHeight
如果已经使用了 status-bar-placeholder,后面的导航栏只需要使用 navBarHeight,不要再把状态栏重复加一次。
错误 2:固定写死 64px
.custom-navbar {
height: 64px;
}
固定值并不一定适用于所有设备。更好的方式是通过计算结果绑定:
<view :style="{ height: `${navBarHeight}px` }" />
错误 3:在 H5 端直接调用微信 API
错误示例:
const menuInfo = uni.getMenuButtonBoundingClientRect()
如果代码需要编译到多个端,应使用条件编译:
// #ifdef MP-WEIXIN
const menuInfo = uni.getMenuButtonBoundingClientRect()
// #endif
错误 4:没有处理失败兜底
真实项目中,API 可能因为调用时机、开发工具模拟环境或基础库差异拿不到有效结果。因此要判断:
if (menuInfo && menuInfo.height > 0) {
// 使用真实数据
} else {
// 使用兜底值
}
错误 5:使用 scroll-view 时高度没有扣除顶部区域
如果 scroll-view 使用固定高度,需要扣除顶部区域:
<scroll-view
scroll-y
:style="{
height: `calc(100vh - ${topAreaHeight}px)`
}"
>
内容
</scroll-view>
如果使用普通页面滚动,则通常只需给顶部区域留出空间,不一定要手动计算滚动容器高度。
十、一个更完整的通用导航栏结构
<template>
<view class="page">
<view :style="{ height: `${statusBarHeight}px` }" />
<view
class="navbar"
:style="{ height: `${navBarHeight}px` }"
>
<view class="navbar-inner">
<view class="navbar-left" @click="goBack">
返回
</view>
<view class="navbar-title">
{{ title }}
</view>
<view
class="navbar-right"
:style="{
width: `${menuButtonInfo?.width || 87}px`
}"
/>
</view>
</view>
<view class="content">
页面内容
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useTopBar } from '@/composables/useTopBar'
const title = ref('页面标题')
const { statusBarHeight, navBarHeight, menuButtonInfo, calculate } = useTopBar()
function goBack() {
uni.navigateBack()
}
onMounted(calculate)
</script>
<style scoped>
.navbar {
box-sizing: border-box;
background: #fff;
}
.navbar-inner {
display: flex;
align-items: center;
height: 100%;
padding: 0 24rpx;
}
.navbar-left,
.navbar-right {
flex-shrink: 0;
}
.navbar-left {
width: 120rpx;
}
.navbar-title {
flex: 1;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
padding: 32rpx;
}
</style>
十一、实战建议:怎样才算“优雅”?
建议 1:把“测量”和“渲染”分离
calculate() 只负责获得尺寸,模板只负责使用尺寸。这样后续修改 UI 时,不需要重写计算逻辑。
建议 2:统一输出三个值
建议工具统一提供:
{
statusBarHeight,
navBarHeight,
topAreaHeight
}
这样不同组件可以按需使用,不需要每个组件自己拼接。
建议 3:真实值优先,默认值兜底
优先使用设备真实数据,只有在 API 不可用时才使用约定值 44。不要把默认值当成所有设备的绝对真值。
建议 4:只在需要时开启自定义导航栏
原生导航栏已经处理了很多系统适配问题。如果只是修改标题文字或颜色,优先考虑原生配置;只有需要沉浸式背景、特殊搜索框、复杂交互时,再使用 navigationStyle: "custom"。
建议 5:至少测试三类环境
发布前建议测试:
- 普通 Android 手机;
- 带刘海或挖孔的 iPhone;
- 微信开发者工具与真机调试;
- 如果是跨端项目,再测试 H5 和 App。
十二、最后总结
微信小程序顶部区域的计算可以记住下面四句话:
- 状态栏高度通过窗口信息获取;
- 导航栏高度根据胶囊按钮的
top、bottom和状态栏高度计算; - 顶部总高度等于状态栏高度加导航栏高度;
- 跨端调用必须使用条件编译,并准备好默认值兜底。
核心代码可以浓缩为:
const windowInfo = uni.getWindowInfo()
const statusBarHeight = windowInfo.statusBarHeight || 0
const menuInfo = uni.getMenuButtonBoundingClientRect()
const navBarHeight = menuInfo.bottom + menuInfo.top - statusBarHeight
const topAreaHeight = statusBarHeight + navBarHeight
当你把这几个尺寸统一封装起来,顶部导航栏就不再是“在某台手机上刚好能用”的固定布局,而会变成一个可复用、可维护、可跨端扩展的基础能力。
参考资料
- uni-app 官方文档:
uni.getWindowInfo() - uni-app 官方文档:页面样式与内置 CSS 变量
- 微信小程序官方能力:胶囊按钮布局信息 API