🧳 我的 React Trip 之旅(2):底部 TabBar 总是高亮错?别慌,我们一起手把手修好它!

34 阅读4分钟

从“点首页却高亮了“我的””到“丝滑切换 + 精准高亮”,只差一个 Layout 和几行路由配置


🌍 开头:那个让我抓狂的 TabBar

你有没有遇到过这种情况?

  • 点击“首页”,底部 Tab 却高亮了“我的”
  • 刷新页面后,Tab 高亮直接消失
  • 路由变了,但 UI 没反应……

我第一次写移动端 App 的底部导航时,就栽在这上面。试了 useStateuseEffect、甚至手动加 class,结果越改越乱。

直到我搞懂了 React Router v6 的核心思想路由即状态,UI 是状态的投影

今天,我就带你从零搭建一个真正可靠的 TabBar,保证:

✅ 点哪个高亮哪个
✅ 刷新页面依然正确
✅ 代码清晰、可维护、不 hack


🛠️ 第一步:先画出我们的“地图”——路由结构长什么样?

TripApp 有四个主要页面:

  1. 首页(Home)  → /
  2. 搜索(Search)  → /search
  3. 行程(Itinerary)  → /itinerary
  4. 我的(Account)  → /account

它们都共享同一个底部 TabBar,也就是说:这四个页面属于“同一层级” ,不需要嵌套在其他页面里。

所以,我们的路由结构应该是这样的:

/           → Home
/search     → Search
/itinerary  → Itinerary
/account    → Account

注意:没有 /home 这种路径!首页就是根路径 /


🏗️ 第二步:用 createBrowserRouter 定义路由(Vite + React Router v6)

首先安装 React Router:

npm install react-router-dom

然后创建 src/router/index.js

// src/router/index.js
import { createBrowserRouter } from 'react-router-dom'
import MainLayout from '@/layouts/MainLayout'
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Itinerary from '@/pages/Itinerary'
import Account from '@/pages/Account'

const router = createBrowserRouter([
  {
    path: '/',
    element: <MainLayout />,
    children: [
      { index: true, element: <Home /> },        // 匹配 /
      { path: 'search', element: <Search /> },   // 匹配 /search
      { path: 'itinerary', element: <Itinerary /> },
      { path: 'account', element: <Account /> }
    ]
  }
])

export default router

🔍 关键点:

  • 所有页面都作为 MainLayout 的 子路由(children)
  • index: true 表示当路径是 / 时,渲染 Home

🎭 第三步:创建 MainLayout —— TabBar 的“家”

MainLayout 是一个布局组件,它负责两件事:

  1. 渲染当前页面的内容(通过 <Outlet />
  2. 在底部固定显示 TabBar
// src/layouts/MainLayout.jsx
import { Outlet } from 'react-router-dom'
import { Tabbar } from 'react-vant'

export default function MainLayout() {
  return (
    <div style={{ paddingBottom: '50px' }}>
      {/* 页面内容会在这里显示 */}
      <Outlet />
      
      {/* 固定在底部的 TabBar */}
      <div style={{
        position: 'fixed',
        bottom: 0,
        left: 0,
        right: 0,
        zIndex: 999
      }}>
        <Tabbar />
      </div>
    </div>
  )
}

Outlet 就像一个“插座”——React Router 会把匹配到的子页面(Home/Search等)自动插进来。


🧩 第四步:让 TabBar “知道”当前在哪一页

现在问题来了:TabBar 怎么知道该高亮哪个图标?

答案是:读取当前 URL 路径

我们用 useLocation() Hook 获取当前路径:

// src/components/MyTabBar.jsx
import { useLocation } from 'react-router-dom'
import { Tabbar } from 'react-vant'

export default function MyTabBar() {
  const location = useLocation()
  const { pathname } = location

  console.log('当前路径:', pathname) // 比如 "/search"

  return (
    <Tabbar value={pathname}>
      <Tabbar.Item
        name="/"
        icon="home-o"
        badge=""
      >
        首页
      </Tabbar.Item>
      <Tabbar.Item
        name="/search"
        icon="search"
      >
        搜索
      </Tabbar.Item>
      <Tabbar.Item
        name="/itinerary"
        icon="todo-list-o"
      >
        行程
      </Tabbar.Item>
      <Tabbar.Item
        name="/account"
        icon="user-o"
      >
        我的
      </Tabbar.Item>
    </Tabbar>
  )
}

🔥 核心原理:

  • Tabbar 的 value 属性决定哪个 Item 高亮
  • 我们把 value 设为当前 pathname(比如 /search
  • 每个 Tabbar.Item 的 name 必须和路径完全一致!

这样,当你访问 /search 时:

  • value="/search"
  • 第二个 Item 的 name="/search" → 自动高亮 ✅

🔄 第五步:点击 Tab 切换页面?用 useNavigate

现在 TabBar 能正确高亮了,但点击图标不会跳转

我们需要给每个 Item 加上点击事件:

import { useLocation, useNavigate } from 'react-router-dom'

export default function MyTabBar() {
  const location = useLocation()
  const navigate = useNavigate()
  const { pathname } = location

  const handleTabChange = (path) => {
    navigate(path)
  }

  return (
    <Tabbar value={pathname} onChange={handleTabChange}>
      <Tabbar.Item name="/" icon="home-o">首页</Tabbar.Item>
      <Tabbar.Item name="/search" icon="search">搜索</Tabbar.Item>
      <Tabbar.Item name="/itinerary" icon="todo-list-o">行程</Tabbar.Item>
      <Tabbar.Item name="/account" icon="user-o">我的</Tabbar.Item>
    </Tabbar>
  )
}

onChange 会把点击的 name(即路径)传给 handleTabChange
navigate(path) 让浏览器跳转,同时触发路由更新 → Outlet 自动切换页面


🧪 测试一下:完美!

  • 打开首页 → Tab 高亮“首页”
  • 点“搜索” → 页面切换 + Tab 高亮“搜索”
  • 刷新 /account → 依然高亮“我的”
  • 手动输入 /itinerary → 正确显示行程页 + 高亮

💡 这就是 声明式 UI 的魅力:我们只描述“当前路径是什么”,UI 自动同步,无需手动管理状态!


🧭 结尾:你已经掌握了 SPA 的核心骨架

现在,你的 TripApp 已经有了:

  • 清晰的路由结构
  • 可复用的布局组件
  • 精准高亮的 TabBar
  • 无刷新的页面切换体验

这就像搭好了房子的框架、水电管线——接下来,往每个房间(页面)里放家具(功能)就轻松多了。

下一站,我会带你实现“首页瀑布流”:如何用 IntersectionObserver 实现图片懒加载?如何避免图片闪烁和布局抖动?