引言
界面效果图部分展示
一个完整的旅游 App 通常包含多个核心页面,如首页、特惠专区、我的收藏、行程管理与个人中心等。为了实现页面之间的高效切换与统一布局管理,本文将基于 React Router 实现多布局路由配置,并结合 react-vant 的 Tabbar 组件,打造一个支持自动高亮和路由跳转的底部导航栏。
通过本文,你将学会如何在 React 中实现优雅的路由管理和页面结构控制。
一、动手开发
1. 底部导航栏的实现
1.1 完整代码
App.jsx
import './App.css'
import {
Suspense,
lazy,
} from 'react'
import {
Routes,
Route,
Navigate,
} from 'react-router-dom'
import MainLayout from '@/components/MainLayout'
import BlankLayout from '@/components/BlankLayout'
const Home = lazy(() => import('@/pages/Home'))
const Search = lazy(() => import('@/pages/Search'))
const Discount = lazy(() => import('@/pages/Discount'))
const Collection = lazy(() => import('@/pages/Collection'))
const Trip = lazy(() => import('@/pages/Trip'))
const Account = lazy(() => import('@/pages/Account'))
function App() {
return (
<>
<Suspense fallback={<div>loading</div>}>
{/* 带有 tabBar 的layout */}
<Routes>
<Route element={<MainLayout />}>
<Route path='/' element={<Navigate to="/home" />} />
<Route path='/home' element={<Home />} />
<Route path='/discount' element={<Discount />} />
<Route path='/collection' element={<Collection />} />
<Route path='/trip' element={<Trip />} />
<Route path='/account' element={<Account />} />
</Route>
{/* 空的layout */}
<Route element={<BlankLayout />}>
<Route path='/search' element={<Search />} />
</Route>
</Routes>
</Suspense>
</>
)
}
export default App
components/MainLayout/index.jsx
import {
useEffect,
useState,
} from 'react';
import {
Tabbar,
} from 'react-vant';
import {
HomeO,
Search,
FriendsO,
SettingO,
UserO
} from '@react-vant/icons';
import {
Outlet,
useNavigate,
useLocation
} from 'react-router-dom'
//菜单栏配置
const tabs = [
{ icon: <HomeO />, title: '首页', path: '/home'},
{ icon: <Search />, title: '特惠专区', path: '/discount'},
{ icon: <FriendsO />, title: '我的收藏', path: '/collection'},
{ icon: <SettingO />, title: '行程', path: '/trip'},
{ icon: <UserO />, title: '我的账户', path: '/account'}
]
const MainLayout = () => {
const [active, setActive] = useState(0)
const navigate = useNavigate()
const location = useLocation()
useEffect(() => {
console.log(location.pathname,'///')
// es6的使用power
const index = tabs.findIndex(
tab => location.pathname.startsWith(tab.path)
)
setActive(index)
}, [])
return (
<div className="flex flex-col h-screen"
style={{paddingBottom: '50px'}}
>
<div className="flex-1">
<Outlet />
</div>
{/* tabbar */}
<Tabbar value={active} onChange={
(key) => {
setActive(key)
navigate(tabs[key].path)
}
}>
{tabs.map((tab, index) => (
<Tabbar.Item
key={index}
icon={tab.icon}
>
{tab.title}
</Tabbar.Item>
))}
</Tabbar>
</div>
)
}
export default MainLayout;
components/BlankLayout/index.jsx
目前还没写,先简单写个样板。
import {
Outlet
} from 'react-router-dom'
const BlankLayout = () => {
return (
<>
<Outlet />
<div>
<h1>BlankLayout</h1>
</div>
</>
)
}
export default BlankLayout
hooks/useTitle.js
设置所有页面的标题,方便复用。
import {
useEffect
} from 'react'
function useTitle(title) {
useEffect(() => {
document.title = title
}, [])
}
export default useTitle
useTitle('旅游智能客服')
2. 代码解释
2.1 整体布局:双布局路由
App 组件使用了两种布局:
MainLayout:用于需要底部导航栏的页面(如首页、我的收藏等)。BlankLayout:用于不需要底部导航的页面(如搜索页)。
通过 react-router-dom 的嵌套路由,不同页面使用不同的布局。
<Route element={<MainLayout />}>
{/* 所有带底部导航的页面 */}
</Route>
<Route element={<BlankLayout />}>
<Route path='/search' element={<Search />} />
{/* 搜索页无底部导航 */}
</Route>
return (
<>
<Outlet /> {*嵌套路由,这里放置其他页面*}
<div>
<h1>BlankLayout</h1>
</div>
</>
)
2.2 底部导航栏实现(MainLayout)
- 状态与路由控制
const [active, setActive] = useState(0) //控制当前激活的 Tab 索引(从 0 开始)
const navigate = useNavigate() //用于编程式跳转路由(来自 react-router-dom)
const location = useLocation() //用于获取当前 URL 路径(用于高亮当前 Tab)。
- 菜单数据定义
tabs数组定义了底部导航的每一项:图标、标题、对应路径。
//菜单栏配置
const tabs = [
{ icon: <HomeO />, title: '首页', path: '/home'},
{ icon: <Search />, title: '特惠专区', path: '/discount'},
{ icon: <FriendsO />, title: '我的收藏', path: '/collection'},
{ icon: <SettingO />, title: '行程', path: '/trip'},
{ icon: <UserO />, title: '我的账户', path: '/account'}
]
- 使用
Tabbar渲染导航
利用react-vant的<Tabbar>和<Tabbar.Item>渲染导航栏。
<Tabbar value={active} onChange={...}>
{tabs.map(...)}
</Tabbar>
本项目实例:
<Tabbar value={active} onChange={ //value={active}:绑定当前激活的 Tab
(key) => {
setActive(key) //更新高亮状态
navigate(tabs[key].path) //跳转到点击的对应路径,实现页面切换
}
}>
{tabs.map((tab, index) => (
<Tabbar.Item
key={index}
icon={tab.icon}
>
{tab.title}
- `value={active}`:绑定当前激活的 Tab。
</Tabbar.Item>
))}
</Tabbar>
- 点击跳转
使用useNavigate()获取跳转函数,点击某一项时,跳转到对应路径:
onChange={(key) => {
setActive(key);
navigate(tabs[key].path);
}}
- 自动高亮当前项
- 页面加载时,通过
location.pathname获取当前路径。 - 使用
findIndex找到tabs中哪个 Tab 的路径与当前路径匹配。 - 设置
active状态为对应索引,实现 Tab 的自动高亮。 startsWith是为了支持子路径(如/trip/detail也能匹配/trip)。
useEffect(() => {
const index = tabs.findIndex(
tab => location.pathname.startsWith(tab.path)
)
setActive(index)
}, [])
- 内容展示
<Outlet />用于渲染当前路由匹配的页面组件(如<Home />)。
2.3 路由跳转(App.jsx)
前面有专门介绍路由的文章:juejin.cn/post/752746… 懒加载:juejin.cn/post/752786…
这里就不一一赘述。
<Suspense fallback={<div>loading</div>}>
{/* 带有 tabBar 的layout */}
<Routes>
<Route element={<MainLayout />}>
<Route path='/' element={<Navigate to="/home" />} />
<Route path='/home' element={<Home />} />
<Route path='/discount' element={<Discount />} />
<Route path='/collection' element={<Collection />} />
<Route path='/trip' element={<Trip />} />
<Route path='/account' element={<Account />} />
</Route>
{/* 空的layout */}
<Route element={<BlankLayout />}>
<Route path='/search' element={<Search />} />
</Route>
</Routes>
</Suspense>
路由懒加载(性能优化)
- 使用
lazy+import()实现组件懒加载,只有访问对应页面时才加载代码。 Suspense fallback在加载中显示“loading”,提升用户体验。
const Home = lazy(() => import('@/pages/Home'))
const Search = lazy(() => import('@/pages/Search'))
const Discount = lazy(() => import('@/pages/Discount'))
const Collection = lazy(() => import('@/pages/Collection'))
const Trip = lazy(() => import('@/pages/Trip'))
const Account = lazy(() => import('@/pages/Account'))
// return
<Suspense fallback={<div>loading</div>}>
....
</Suspense>
结尾
本旅游App项目目前介绍到这,后面还会接着出喔,感兴趣的小伙伴可以继续关注~