噜噜旅游App(2)——路由系统与底部导航栏实现

124 阅读4分钟

引言

界面效果图部分展示

image.png 一个完整的旅游 App 通常包含多个核心页面,如首页、特惠专区、我的收藏、行程管理与个人中心等。为了实现页面之间的高效切换与统一布局管理,本文将基于 React Router 实现多布局路由配置,并结合 react-vant 的 Tabbar 组件,打造一个支持自动高亮和路由跳转的底部导航栏。

通过本文,你将学会如何在 React 中实现优雅的路由管理和页面结构控制。

一、动手开发

1. 底部导航栏的实现

image.png

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('旅游智能客服')

image.png

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
  1. 状态与路由控制
const [active, setActive] = useState(0) //控制当前激活的 Tab 索引(从 0 开始)
const navigate = useNavigate() //用于编程式跳转路由(来自 react-router-dom)
const location = useLocation() //用于获取当前 URL 路径(用于高亮当前 Tab)。
  1. 菜单数据定义
    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'}
]
  1. 使用 Tabbar 渲染导航
    利用 react-vant<Tabbar><Tabbar.Item> 渲染导航栏。
<Tabbar value={active} onChange={...}>
  {tabs.map(...)}
</Tabbar>

image.png

本项目实例:

<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>
  1. 点击跳转
    使用 useNavigate() 获取跳转函数,点击某一项时,跳转到对应路径:
onChange={(key) => {
  setActive(key);
  navigate(tabs[key].path);
}}
  1. 自动高亮当前项
  • 页面加载时,通过 location.pathname 获取当前路径。
  • 使用 findIndex 找到 tabs 中哪个 Tab 的路径与当前路径匹配。
  • 设置 active 状态为对应索引,实现 Tab 的自动高亮
  • startsWith 是为了支持子路径(如 /trip/detail 也能匹配 /trip)。
 useEffect(() => {
    const index = tabs.findIndex(
        tab => location.pathname.startsWith(tab.path)
    )
    setActive(index)
}, [])
  1. 内容展示
    <Outlet /> 用于渲染当前路由匹配的页面组件(如 <Home />)。 image.png
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项目目前介绍到这,后面还会接着出喔,感兴趣的小伙伴可以继续关注~