惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Securelist
L
LangChain Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
月光博客
月光博客
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
美团技术团队
S
Security @ Cisco Blogs
人人都是产品经理
人人都是产品经理
L
LINUX DO - 热门话题
S
SegmentFault 最新的问题
D
DataBreaches.Net
C
CXSECURITY Database RSS Feed - CXSecurity.com
Microsoft Security Blog
Microsoft Security Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
N
News | PayPal Newsroom
W
WeLiveSecurity
F
Fortinet All Blogs
The Hacker News
The Hacker News
The Register - Security
The Register - Security
P
Palo Alto Networks Blog
Engineering at Meta
Engineering at Meta
T
The Exploit Database - CXSecurity.com
Vercel News
Vercel News
G
GRAHAM CLULEY
博客园 - 聂微东
P
Privacy International News Feed
P
Privacy & Cybersecurity Law Blog
Hacker News: Ask HN
Hacker News: Ask HN
Webroot Blog
Webroot Blog
G
Google Developers Blog
T
Tailwind CSS Blog
V
Vulnerabilities – Threatpost
L
Lohrmann on Cybersecurity
T
Tenable Blog
Cisco Talos Blog
Cisco Talos Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Security Affairs
云风的 BLOG
云风的 BLOG
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
B
Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
T
Threatpost

吖远zzyの博客

记录一下豆包视频去水印的研究过程,以及fallback_api加密数据qAAB开头数据的解密。 | 吖远zzyの博客 解决华为开发者工具 DevEco Studio 登录跳转 localhost:10101 端口被阻止问题 | 吖远zzyの博客 微信小程序个人资质审核血泪史:踩坑,从反复被拒到神奇过审 | 吖远zzyの博客 如何实现公众号自动回复或做一个短视频无水印解析机器人? | 吖远zzyの博客 以后在手机上用 Termux 和 GitHub Actions 更新 Hexo 博客 | 吖远zzyの博客 必备!这款万能小程序能给抖音/豆包视频图集去除水印、还有有趣的测反应、玩成语模拟工具。 自制的一些免费API接口第二弹(获取QQ昵称、头像、抖音/豆包视频去水印等...) 在安卓手机上运行OpenClaw?当然可以,用Termux折腾OpenClaw安装QQ机器人的踩坑记录... | 吖远zzyの博客 个人开发工具之抖音直播录制工具:一款功能强大的Android直播录制应用 UniApp中Canvas绘图的易错点与踩坑指南 QQ频道机器人与UniApp开发:常见踩坑点与解决方案 UniApp中Canvas绘图不显示的常见问题与解决方案 Hexo博客实现随机文章功能的完整教程 QQ频道机器人Android客户端使用指南 | 吖远zzyの博客 安卓版QQ频道机器人APP客户端插件开发指南 | 吖远zzyの博客 2024年AI编程助手深度评测:哪款最适合你? uni-app地图定位踩坑记:地图功能和定位的那些坑 uni-app文件上传踩坑记:图片处理和上传全攻略 uni-app表单验证踩坑记:这些坑我替你踩过了 uni-app开发踩坑记录:新手必看的常见问题与解决方案 uni-app安全防护指南:构建可靠的跨端应用 uni-app性能优化指南:从加载到渲染的全方位提升 uni-app动画效果实战:从基础到高级的动画实现指南 uni-app组件开发实战:从基础到进阶的最佳实践 uni-app网络请求与缓存策略:构建高效的数据层 uni-app状态管理进阶:Vuex最佳实践与性能优化 uni-app路由与页面跳转:最佳实践与踩坑指南
告别原生导航栏:微信小程序自定义导航栏完美适配方案
吖远zzy · 2025-11-25 · via 吖远zzyの博客
  1. 1. 为什么需要自定义导航栏?
    1. 1.1. 基础配置:开启自定义模式
    2. 1.2. 核心技术:动态获取设备信息
    3. 1.3. 封装导航栏组件 (NavBar)
      1. 1.3.0.1. 1. 组件代码结构
      2. 1.3.0.2. 2. index.js 核心逻辑
      3. 1.3.0.3. 3. index.wxml 结构
      4. 1.3.0.4. 4. index.wxss 样式
      5. 1.3.0.5. 5. 注册组件
  2. 1.4. 页面中使用导航栏
    1. 1.4.0.1. 1. 页面的 WXML
    2. 1.4.0.2. 2. 页面的 JS (可选)
  • 2. 避坑指南与性能优化
    1. 2.1. ✅ 必须避开的坑
    2. 2.2. 🚀 性能优化建议
  • 3. 总结
  • 告别原生导航栏:微信小程序自定义导航栏完美适配方案 0 次阅读

    为什么需要自定义导航栏?

    原生小程序导航栏虽然开箱即用,但在实际开发中常常遇到以下痛点:

    • 设计风格受限:无法自由定制背景、字体颜色、高度,难以融入品牌设计
    • 扩展能力弱:无法在导航栏区域增加搜索框、自定义按钮、动态效果等
    • 无法实现沉浸式:标题栏与状态栏分离感强,缺少整体感
    • 屏幕适配难:不同机型(刘海屏、挖孔屏、动态岛)的高度表现不一致

    因此,自定义导航栏成为许多商业项目和小程序框架的标配。本文将手把手教你实现一套适配所有主流机型、易扩展、性能优良的自定义导航栏组件。

    基础配置:开启自定义模式

    app.json 中将 navigationStyle 设置为 custom

    1
    2
    3
    4
    5
    6
    7
    8

    {
    "window": {
    "navigationStyle": "custom",
    "navigationBarTextStyle": "black" // 仅对页面内文本颜色有影响,导航栏已隐藏
    }
    }

    💡 小贴士:也可以单独为某个页面配置,在页面目录的 .json 文件中设置相同字段即可。

    配置完成后,小程序所有页面将不再渲染原生导航栏,状态栏区域完全暴露,需要我们自己实现导航UI和内容区域的避让。

    核心技术:动态获取设备信息

    实现完美适配的关键在于准确获取两个高度:

    1. 状态栏高度 (statusBarHeight)
    2. 胶囊按钮位置信息 (menuButtonBoundingClientRect)

    获取关键数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 在 app.js 或页面/组件的 onLoad/attached 中执行
    const systemInfo = wx.getSystemInfoSync();
    const menuButtonInfo = wx.getMenuButtonBoundingClientRect();

    const { statusBarHeight } = systemInfo;
    const { top, height } = menuButtonInfo;

    // 计算导航栏内容区域高度(通常等于 胶囊距顶间距*2 + 胶囊高度 )
    // 间距 = 胶囊顶部 - 状态栏高度
    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重新获取(某些机型旋转屏幕后可能需要)
    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
    });

    // 通过事件将总高度传递给父页面,方便页面内容区域设置padding-top(非必须,组件内部已有占位view)
    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(防止内容被固定导航栏遮挡) -->
    <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;
    /* 可选毛玻璃效果 */
    /* backdrop-filter: blur(10px); */
    }

    .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。

    扩展进阶:动态背景与滚动渐变

    有时候我们需要导航栏随着页面滚动而改变背景透明度(例如从透明逐渐变为白色)。这时需要:

      1. 页面监听 onPageScroll 事件
      1. 将滚动偏移量传递给导航栏组件
      1. 组件内部动态计算背景色的rgba值

    由于篇幅有限,这个高级用法留作课后作业,感兴趣的读者可以尝试实现。

    总结

    自定义导航栏让小程序的设计和交互更加自由,而通过 wx.getMenuButtonBoundingClientRect 和动态高度计算,我们可以做到一套代码完美适配所有机型。

    本文提供的组件已在实际项目中经过多轮测试,兼容以下场景:

    · iOS(刘海屏、动态岛、非全面屏)
    · Android(挖孔屏、水滴屏、全面屏)
    · 折叠屏(展开/折叠状态需单独适配resize事件)
    · iPad / 平板(状态栏高度变化)

    只需复制代码稍作样式调整,即可集成到你的小程序中。