从“点首页却高亮了“我的””到“丝滑切换 + 精准高亮”,只差一个 Layout 和几行路由配置
🌍 开头:那个让我抓狂的 TabBar
你有没有遇到过这种情况?
- 点击“首页”,底部 Tab 却高亮了“我的”
- 刷新页面后,Tab 高亮直接消失
- 路由变了,但 UI 没反应……
我第一次写移动端 App 的底部导航时,就栽在这上面。试了 useState、useEffect、甚至手动加 class,结果越改越乱。
直到我搞懂了 React Router v6 的核心思想:路由即状态,UI 是状态的投影。
今天,我就带你从零搭建一个真正可靠的 TabBar,保证:
✅ 点哪个高亮哪个
✅ 刷新页面依然正确
✅ 代码清晰、可维护、不 hack
🛠️ 第一步:先画出我们的“地图”——路由结构长什么样?
TripApp 有四个主要页面:
- 首页(Home) →
/ - 搜索(Search) →
/search - 行程(Itinerary) →
/itinerary - 我的(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 是一个布局组件,它负责两件事:
- 渲染当前页面的内容(通过
<Outlet />) - 在底部固定显示 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 实现图片懒加载?如何避免图片闪烁和布局抖动?