Files
soul-yongping/开发文档/8、部署/自定义导航组件方案.md

16 KiB
Raw Blame History

自定义导航组件方案

📋 背景

为什么不使用原生 tabBar

需求

  • "找伙伴"功能需要根据 API 配置动态显示/隐藏
  • 不同环境(开发/测试/生产)可能有不同的功能开关
  • 需要灵活控制导航栏的显示逻辑

原生 tabBar 的限制

  1. 静态配置app.json 中的 tabBar.list 是固定的,无法动态增删
  2. 无法隐藏单个 tab:只能显示或隐藏整个 tabBar
  3. 样式受限:虽然支持自定义 tabBar但配置复杂
  4. 功能限制:自定义 tabBar 需要在每个页面手动管理状态

自定义组件的优势

  1. 完全动态:可以根据任何条件显示/隐藏任意 tab
  2. 样式自由:完全控制样式,可以实现中间凸起按钮等特殊效果
  3. 状态统一:通过 props 传递当前页面,组件内部管理激活态
  4. 跨平台一致Web 和小程序使用相同的组件逻辑

🔧 技术方案

方案对比

特性 原生 tabBar 自定义 tabBar 自定义组件(当前方案)
动态显示/隐藏 ⚠️ 复杂 简单
样式自由度 受限 自由 完全自由
中间凸起按钮 不支持 ⚠️ 复杂 简单
跨平台一致性 不同 ⚠️ 需额外处理 一致
页面跳转 wx.switchTab wx.switchTab wx.reLaunch
配置复杂度 简单 复杂 中等

结论:使用自定义组件方案


📝 实现细节

1. 移除原生 tabBar 配置

文件newpp/build/miniprogram.config.js

appExtraConfig: {
  sitemapLocation: 'sitemap.json',
  // ✅ 不配置 tabBar使用完全自定义的导航组件
  // 原因:需要根据 API 配置动态显示/隐藏"找伙伴"功能
},

说明

  • 不在 app.json 中配置 tabBar
  • 完全依赖自定义组件 BottomNav.jsx

2. 修改路由跳转方式

文件newpp/src/adapters/router.js

Before使用 wx.switchTab

export function switchTab(path) {
  if (isMiniProgram()) {
    wx.switchTab({ url: toMpPath(path) }) // ❌ 需要原生 tabBar 配置
  } else {
    window.location.href = path === '/' ? 'index.html' : path.replace(/^\//, '') + '.html'
  }
}

After使用 wx.reLaunch

export function switchTab(path) {
  if (isMiniProgram()) {
    // ✅ 使用 wx.reLaunch 代替 wx.switchTab
    // 原因:没有配置原生 tabBar使用自定义组件
    wx.reLaunch({ url: toMpPath(path) })
  } else {
    window.location.href = path === '/' ? 'index.html' : path.replace(/^\//, '') + '.html'
  }
}

wx.reLaunch vs wx.switchTab

API 说明 页面栈 限制 适用场景
wx.switchTab 跳转到 tabBar 页面 清空 只能跳转到原生 tabBar 页面 原生 tabBar
wx.reLaunch 关闭所有页面,打开某页面 清空 自定义导航
wx.redirectTo 关闭当前页面,跳转 替换栈顶 页面跳转
wx.navigateTo 保留当前页面,跳转 新增栈 最多10层 详情页

为什么选择 wx.reLaunch

  1. 清空页面栈,避免页面堆积
  2. 无限制,可以跳转到任何页面
  3. 模拟 tabBar 的行为(清空栈)
  4. ⚠️ 缺点:每次跳转会重新加载页面(但对于导航栏切换这是正常的)

3. 自定义组件实现

文件newpp/src/components/BottomNav.jsx

核心逻辑

export default function BottomNav({ current }) {
  const [matchEnabled, setMatchEnabled] = useState(true)
  const [configLoaded, setConfigLoaded] = useState(false)

  useEffect(() => {
    if (isMiniProgram()) {
      // ✅ 小程序:从 app.globalData 读取配置
      try {
        const app = getApp()
        if (app && app.globalData) {
          setMatchEnabled(app.globalData.matchEnabled !== false)
        }
      } catch (e) {
        // 默认显示
      } finally {
        setConfigLoaded(true)
      }
    } else {
      // ✅ Web从 API 加载配置
      fetch('/api/db/config')
        .then((res) => res.json())
        .then((data) => {
          if (data.features) {
            setMatchEnabled(data.features.matchEnabled === true)
          }
        })
        .catch(() => {
          setMatchEnabled(false)
        })
        .finally(() => {
          setConfigLoaded(true)
        })
    }
  }, [])

  // ✅ 根据配置动态生成可见的 tabs
  const visibleTabs = matchEnabled ? tabs : tabs.filter((t) => t.id !== 'match')

  const handleTabClick = (path) => {
    if (path === current) return
    
    // ✅ 使用 switchTab内部调用 wx.reLaunch
    try {
      switchTab(path)
    } catch (e) {
      console.error('switchTab error:', e)
    }
  }

  return (
    <div style={styles.nav}>
      <div style={styles.container}>
        {visibleTabs.map((tab) => {
          // ✅ 根据 isCenter 渲染不同样式
          if (tab.isCenter) {
            return (/* 中间凸起按钮 */)
          }
          return (/* 普通按钮 */)
        })}
      </div>
    </div>
  )
}

配置加载流程

┌─────────────────┐
│  组件挂载       │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  判断环境       │
│  isMiniProgram? │
└────┬──────┬─────┘
     │ Yes  │ No
     ▼      ▼
┌─────────┐ ┌──────────────┐
│小程序环境│ │  Web 环境    │
│getApp()  │ │fetch('/api') │
│.globalData│ │  .then()    │
└────┬────┘ └──────┬───────┘
     │             │
     └──────┬──────┘
            ▼
    ┌───────────────┐
    │setMatchEnabled│
    │setConfigLoaded│
    └───────┬───────┘
            ▼
    ┌───────────────┐
    │ 动态渲染 tabs │
    │ visibleTabs   │
    └───────────────┘

🎯 配置管理

Web 环境

API 端点/api/db/config

返回格式

{
  "features": {
    "matchEnabled": true  // ✅ 控制"找伙伴"功能
  }
}

配置位置:数据库或配置文件

// app/api/db/config/route.ts
export async function GET() {
  const config = await db.collection('config').findOne({ key: 'features' })
  return NextResponse.json({
    features: {
      matchEnabled: config?.matchEnabled ?? true, // 默认开启
    },
  })
}

小程序环境

配置位置miniprogram/app.js

App({
  globalData: {
    matchEnabled: true, // ✅ 控制"找伙伴"功能
    // 其他配置...
  },
  
  onLaunch() {
    // ✅ 可以从服务器加载配置
    this.loadFeatureConfig()
  },
  
  async loadFeatureConfig() {
    try {
      const res = await wx.request({
        url: 'https://your-api.com/config',
        method: 'GET',
      })
      
      if (res.data && res.data.features) {
        this.globalData.matchEnabled = res.data.features.matchEnabled
        
        // ✅ 触发页面更新(如果需要)
        this.notifyConfigUpdate()
      }
    } catch (e) {
      console.error('Load config error:', e)
    }
  },
  
  notifyConfigUpdate() {
    // 通知所有页面配置已更新
    // 可以使用事件总线或全局状态管理
  },
})

📱 页面集成

在每个导航页面中使用

示例newpp/src/pages/HomePage.jsx

import BottomNav from '../components/BottomNav'

export default function HomePage() {
  return (
    <div>
      <div style={styles.page}>
        {/* 页面内容 */}
      </div>
      
      {/* ✅ 传入当前路径 */}
      <BottomNav current="/" />
    </div>
  )
}

其他页面

  • ChaptersPage.jsx: <BottomNav current="/chapters" />
  • MatchPage.jsx: <BottomNav current="/match" />
  • MyPage.jsx: <BottomNav current="/my" />

🐛 已知问题与解决方案

问题 1页面切换时闪烁

症状:使用 wx.reLaunch 时,页面会重新加载,可能出现白屏

原因wx.reLaunch 会关闭所有页面,然后打开新页面

解决方案

  1. 方案 A优化页面加载速度

    • 使用骨架屏
    • 预加载数据
    • 缓存页面状态
  2. 方案 B使用页面栈管理推荐

    export function switchTab(path) {
      if (isMiniProgram()) {
        const pages = getCurrentPages()
        const currentPage = pages[pages.length - 1]
        const currentPath = '/' + currentPage.route.replace(/^pages\//, '').replace(/\/index$/, '')
    
        // ✅ 如果是相同页面,不跳转
        if (currentPath === path) {
          return
        }
    
        // ✅ 检查页面栈中是否已有目标页面
        const targetPageIndex = pages.findIndex((p) => {
          const pPath = '/' + p.route.replace(/^pages\//, '').replace(/\/index$/, '')
          return pPath === path
        })
    
        if (targetPageIndex >= 0) {
          // ✅ 如果已有,返回到该页面
          const delta = pages.length - 1 - targetPageIndex
          wx.navigateBack({ delta })
        } else {
          // ✅ 如果没有,使用 reLaunch
          wx.reLaunch({ url: toMpPath(path) })
        }
      } else {
        window.location.href = path === '/' ? 'index.html' : path.replace(/^\//, '') + '.html'
      }
    }
    
  3. 方案 C保留部分页面栈

    export function switchTab(path) {
      if (isMiniProgram()) {
        const pages = getCurrentPages()
    
        // ✅ 如果栈中只有1个页面使用 redirectTo
        if (pages.length === 1) {
          wx.redirectTo({ url: toMpPath(path) })
        } else {
          // ✅ 否则,返回到第一个页面,然后 redirectTo
          wx.navigateBack({ delta: pages.length - 1 })
          setTimeout(() => {
            wx.redirectTo({ url: toMpPath(path) })
          }, 100)
        }
      } else {
        window.location.href = path === '/' ? 'index.html' : path.replace(/^\//, '') + '.html'
      }
    }
    

推荐:方案 B检查页面栈


问题 2配置更新不及时

症状:修改 app.globalData.matchEnabled 后,导航栏不更新

原因:组件已挂载,useEffect 只执行一次

解决方案

  1. 方案 A监听配置变化

    useEffect(() => {
      if (isMiniProgram()) {
        const app = getApp()
    
        // ✅ 监听配置变化
        const checkConfig = () => {
          if (app && app.globalData) {
            setMatchEnabled(app.globalData.matchEnabled !== false)
          }
        }
    
        // ✅ 初始加载
        checkConfig()
    
        // ✅ 定时检查(或使用事件监听)
        const interval = setInterval(checkConfig, 1000)
    
        return () => clearInterval(interval)
      }
    }, [])
    
  2. 方案 B使用全局状态管理

    // store/index.js
    const useStore = create((set) => ({
      matchEnabled: true,
      setMatchEnabled: (enabled) => set({ matchEnabled: enabled }),
    }))
    
    // BottomNav.jsx
    const { matchEnabled } = useStore()
    
  3. 方案 C页面激活时刷新

    useEffect(() => {
      const handleShow = () => {
        // ✅ 页面显示时重新读取配置
        if (isMiniProgram()) {
          const app = getApp()
          if (app && app.globalData) {
            setMatchEnabled(app.globalData.matchEnabled !== false)
          }
        }
      }
    
      // ✅ 监听页面显示事件
      if (isMiniProgram() && typeof wx !== 'undefined') {
        wx.onAppShow?.(handleShow)
      }
    
      return () => {
        wx.offAppShow?.(handleShow)
      }
    }, [])
    

推荐:方案 C页面激活时刷新


问题 3Web 和小程序跳转体验不一致

症状Web 使用 location.href 会刷新整个页面,小程序使用 wx.reLaunch 有过渡动画

原因Web 和小程序的路由机制不同

解决方案

  1. Web 端使用 SPA 路由(如果是 Next.js

    import { useRouter } from 'next/router'
    
    export function switchTab(path) {
      if (isMiniProgram()) {
        wx.reLaunch({ url: toMpPath(path) })
      } else {
        // ✅ 使用 Next.js 路由
        const router = useRouter()
        router.push(path)
      }
    }
    
  2. 小程序端优化过渡

    export function switchTab(path) {
      if (isMiniProgram()) {
        wx.reLaunch({
          url: toMpPath(path),
          success: () => {
            // ✅ 跳转成功
          },
          fail: (err) => {
            console.error('reLaunch fail:', err)
            // ✅ 降级到 navigateTo
            wx.navigateTo({ url: toMpPath(path) })
          },
        })
      } else {
        window.location.href = path === '/' ? 'index.html' : path.replace(/^\//, '') + '.html'
      }
    }
    

测试清单

功能测试

  • 配置加载:小程序启动时正确读取 matchEnabled
  • 动态显示/隐藏
    • matchEnabled: true 时,显示"找伙伴" tab
    • matchEnabled: false 时,隐藏"找伙伴" tab
  • 页面跳转
    • 点击"首页" → 跳转到首页
    • 点击"目录" → 跳转到目录页
    • 点击"找伙伴" → 跳转到找伙伴页
    • 点击"我的" → 跳转到我的页
  • 激活态:当前页 tab 高亮显示

样式测试

  • 中间凸起按钮:渐变色 + 阴影
  • 普通按钮:灰色/高亮切换
  • 安全区适配:底部留出安全区高度

性能测试

  • 配置加载速度< 100ms
  • 页面跳转速度< 500ms
  • 内存占用:无内存泄漏

📊 对比总结

方案 原生 tabBar 自定义 tabBar 自定义组件(当前)
动态控制 ⚠️
样式自由
配置复杂度
跳转方式 wx.switchTab wx.switchTab wx.reLaunch
页面栈 清空 清空 清空
跨平台一致性 ⚠️
维护成本

结论:自定义组件方案最适合当前需求


🚀 后续优化

Priority P1推荐

  1. 优化页面跳转体验

    • 实现方案 B页面栈检查
    • 避免不必要的页面重新加载
  2. 配置热更新

    • 实现配置变化监听
    • 自动更新导航栏显示
  3. 添加骨架屏

    • 减少页面切换时的白屏
    • 提升用户体验

Priority P2可选

  1. 图标优化

    • 使用图片替换 emoji
    • 支持激活态和非激活态
  2. 动效优化

    • 添加页面切换动画
    • 优化按钮点击反馈
  3. 埋点统计

    • 记录 tab 点击次数
    • 分析用户行为

📚 相关文档

  1. 小程序样式修复说明
  2. 小程序底部导航修复说明
  3. Kbone踩坑修复指南

总结:使用自定义组件方案,完全控制导航栏的显示逻辑,满足根据 API 配置动态显示/隐藏"找伙伴"功能的需求。