微信小程序自定义 tabBar 实战:custom-tab-bar 从适配到深色模式

微信小程序自定义 tabBar 实战:custom-tab-bar 从适配到深色模式

admin
2026-05-12 / 0 评论 / 255 阅读

微信小程序自定义 tabBar 实战:custom-tab-bar 从适配到深色模式

原生 tabBar 配置简单,但样式自由度太低——中间凸起按钮、自定义字体图标、深色模式适配、消息红点动画,这些需求 app.json 里的 tabBar 都做不到。好在微信提供了 custom-tab-bar 方案。这篇把完整的落地方案和那些官方文档没写的坑都过一遍。

一、为什么要自定义 tabBar

先看原生 tabBar 的三个硬伤:

  1. 图标只能是本地图片,不支持 iconfont,多色图标要切两套(普通/选中)
  2. 中间凸起样式做不了,电商类 App 常见的"发布"大按钮没法实现
  3. 红点/角标能力弱wx.setTabBarBadge 只能显示数字,做不了小红点+动画

而 custom-tab-bar 的原理是:app.json 里开启 "custom": true 后,每个 tabBar 页面底部会渲染一个独立的组件实例,完全由你自己实现。

二、基础搭建

2.1 开启配置

// app.json
{
  "tabBar": {
    "custom": true,
    "color": "#666666",
    "selectedColor": "#07C160",
    "backgroundColor": "#ffffff",
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "pages/category/category", "text": "分类" },
      { "pagePath": "pages/publish/publish", "text": "发布" },
      { "pagePath": "pages/message/message", "text": "消息" },
      { "pagePath": "pages/mine/mine", "text": "我的" }
    ]
  }
}

注意:即使全部自定义,list 字段仍然要完整声明——微信靠它识别哪些页面是 tabBar 页面。color 等字段也建议保留,作为兜底(自定义组件加载失败时会降级显示原生 tabBar)。

2.2 创建组件目录

目录名固定为 custom-tab-bar,位置在项目根目录(与 pages 平级,不是组件目录):

├── custom-tab-bar/
│   ├── index.js
│   ├── index.json
│   ├── index.wxml
│   └── index.wxss
├── pages/
└── app.json
// custom-tab-bar/index.json
{
  "component": true
}

2.3 组件实现

// custom-tab-bar/index.js
Component({
  data: {
    selected: 0,
    color: '#666666',
    selectedColor: '#07C160',
    list: [
      { pagePath: '/pages/index/index', text: '首页', icon: 'home' },
      { pagePath: '/pages/category/category', text: '分类', icon: 'category' },
      { pagePath: '/pages/publish/publish', text: '发布', icon: 'publish', center: true },
      { pagePath: '/pages/message/message', text: '消息', icon: 'message', badge: true },
      { pagePath: '/pages/mine/mine', text: '我的', icon: 'mine' }
    ]
  },
  methods: {
    switchTab(e) {
      const url = '/' + e.currentTarget.dataset.path
      wx.switchTab({ url })
    }
  }
})
<!-- custom-tab-bar/index.wxml -->
<view class="tab-bar">
  <view
    wx:for="{{list}}"
    wx:key="pagePath"
    class="tab-item {{item.center ? 'center' : ''}}"
    data-path="{{item.pagePath}}"
    data-index="{{index}}"
    bindtap="switchTab"
  >
    <!-- 中间凸起按钮 -->
    <view wx:if="{{item.center}}" class="center-btn">
      <text class="iconfont icon-{{item.icon}}"></text>
    </view>

    <!-- 普通按钮 -->
    <block wx:else>
      <view class="icon-wrap">
        <text class="iconfont icon-{{item.icon}}"></text>
        <view wx:if="{{item.badge && unreadCount > 0}}" class="badge">{{unreadCount}}</view>
      </view>
      <view class="text" style="color: {{selected === index ? selectedColor : color}}">
        {{item.text}}
      </view>
    </block>
  </view>
</view>

三、最大的坑:每个页面一个独立实例

这是 custom-tab-bar 最反直觉的地方:每个 tabBar 页面都有自己独立的一份 tabBar 组件实例

表现出来的 bug 是:从首页切到"消息"页,tabBar 上的选中态不更新,还停留在"首页"。

3.1 解决方案:页面 onShow 同步选中态

// pages/message/message.js
Page({
  onShow() {
    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
      this.getTabBar().setData({
        selected: 3  // 当前页在 list 中的索引
      })
    }
  }
})

每个 tabBar 页面的 onShow 都要写这段。可以封装一个高阶函数减少重复:

// utils/tab-bar.js
function withTabBar(pageConfig, tabIndex) {
  const originalOnShow = pageConfig.onShow
  pageConfig.onShow = function () {
    if (typeof this.getTabBar === 'function' && this.getTabBar()) {
      this.getTabBar().setData({ selected: tabIndex })
    }
    originalOnShow && originalOnShow.call(this)
  }
  return pageConfig
}

module.exports = { withTabBar }

// 页面使用
Page(withTabBar({
  // 原有配置
}, 3))

3.2 实例隔离带来的另一个问题:状态不同步

消息红点数量存在全局状态里,但 tabBar 是多个实例——首页实例更新了 unreadCount,消息页的实例还是旧值。

解法是把未读数等共享状态放到全局 store 或本地缓存,每个实例在 attached 生命周期里读取:

// custom-tab-bar/index.js
Component({
  lifetimes: {
    attached() {
      const app = getApp()
      this.setData({ unreadCount: app.globalData.unreadCount })
    }
  }
})

配合一个极简的发布订阅,让所有实例响应式更新:

// custom-tab-bar/index.js
Component({
  lifetimes: {
    attached() {
      const app = getApp()
      this._onUnreadChange = (count) => this.setData({ unreadCount: count })
      app.on('unreadChange', this._onUnreadChange)
    },
    detached() {
      getApp().off('unreadChange', this._onUnreadChange)
    }
  }
})

四、胶囊按钮对齐

自定义 tabBar 后,页面内容区的高度计算会变复杂,尤其要处理和右上角胶囊按钮的对齐关系。

获取胶囊位置信息:

// custom-tab-bar/index.js
Component({
  lifetimes: {
    attached() {
      const menuButton = wx.getMenuButtonBoundingClientRect()
      const systemInfo = wx.getSystemInfoSync()
      // tabBar 整体高度 = 胶囊底部 + 上间距 + 内容高度
      const tabBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + 50
      this.setData({ tabBarHeight })
    }
  }
})

自定义导航栏页面同样需要这段逻辑,把 tabBarHeight 存到全局,页面 onLoad 时读取,保证自定义导航栏和 tabBar 视觉上同一套高度体系。

五、深色模式适配

app.json 开启 "darkmode": true 后,自定义 tabBar 不会自动变色,需要手动监听:

// app.json
{
  "darkmode": true,
  "themeLocation": "theme.json"
}
// custom-tab-bar/index.js
Component({
  data: {
    theme: 'light'
  },
  lifetimes: {
    attached() {
      const app = getApp()
      this.setData({ theme: app.globalData.theme || 'light' })
      // 监听系统主题切换
      this._onThemeChange = ({ theme }) => this.setData({ theme })
      wx.onThemeChange(this._onThemeChange)
    },
    detached() {
      wx.offThemeChange(this._onThemeChange)
    }
  }
})

WXSS 里用 CSS 变量切换:

/* custom-tab-bar/index.wxss */
.tab-bar {
  --bg: #ffffff;
  --text: #666666;
  background: var(--bg);
}
.tab-bar.dark {
  --bg: #1f1f1f;
  --text: #999999;
  background: var(--bg);
}

:微信开发者工具模拟深色模式在部分版本有 bug,真机预览才准。另外 iOS 上 wx.onThemeChange 回调时机比页面 onShow 晚,首次进入深色模式下的页面会闪一下白色——可以在 app.jsonLaunch 里提前用 wx.getSystemInfoSync().theme 初始化一次。

六、性能与体验细节

  1. tabBar 组件不要放业务请求。它是每个 tab 页都要实例化的组件,接口请求放这里会导致切换 tab 重复请求。只做状态展示。
  2. 切页动画的"延迟感"wx.switchTab 本身有页面切换开销,如果再在 tabBar 的 tap 回调里做动画,会显得卡。建议 tap 时立即更新本组件的选中态,不要等页面 onShow 回来再切:
switchTab(e) {
  const { path, index } = e.currentTarget.dataset
  this.setData({ selected: index })  // 立即切换,不等 onShow
  wx.switchTab({ url: '/' + path })
}

页面 onShow 里的同步逻辑保留作为兜底(覆盖 wx.switchTab API 直接调用、其他页面跳转回来的场景)。

  1. 中间凸起按钮的点击区域。凸出的部分超出了 tabBar 容器,注意 overflow: hidden 别加在外层,同时用 padding 扩大热区到 88rpx 以上。

七、避坑清单

问题原因解法
选中态不更新每个 tab 页独立实例各页面 onShow 里 getTabBar().setData
红点数不同步实例间状态隔离全局发布订阅 or 本地缓存
首次进入闪白主题初始化晚app.js onLaunch 提前读 theme
真机不显示 tabBar目录名/位置错误必须是根目录 custom-tab-bar
切 tab 闪烁setData 时序tap 时先本地切选中态
图片资源路径失效组件内相对路径用绝对路径 /images/xxx

写在最后

custom-tab-bar 的本质是"把 tabBar 当成一个跨页面共享的组件来管理"。理解了多实例这个核心设定,选中态同步、状态共享、主题响应这些问题的方案就都顺理成章了。如果你的项目 tab 样式并不复杂,原生 tabBar + wx.setTabBarItem 动态改文案其实也够用,不要为了自定义而自定义。

0

评论 (0)

取消
0:00