三、React-路由

177 阅读12分钟

1.路由介绍

使用 react 构建的单页面应用,想要实现页面间的跳转,想要随着 url 地址的切换显示不同的内容,就是使用路由。在 react 中有两个包可以实现这个需求,react-router 和 react-router-dom。

react-router:实现了路由的核心功能。

react-router-dom:基于 react-router 加入了在浏览器运行环境下的一些功能。分为v5版本和v6版本,下面将介绍两种版本的用法。

2.路由的使用 v5版本

2.1 安装

npm i react-router-dom@5

2.2 定义路由规则

在使用之前先要用 react-router-dom 提供的两个组件 BrowserRouter或者HashRouter 包裹跟组件。

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter,HashRouter } from 'react-router-dom';
// BrowserRouter,HashRouter 是实现的是history和hash两种路由模式
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  // <React.StrictMode>
  <BrowserRouter>
     //<HashRouter>
    <App />
    //</HashRouter>
   </BrowserRouter> 
  // </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

通过路由组件来定义路由规则:

Switch 使用Switch组件包裹路由规则,匹配到路由之后就不继续往下匹配了。

Route 设置路由(path路径,component组件)。通过将path设置为 * 来添加404路由。

Redirect 设置路由重定向,可以添加exact,让路由严格匹配(当路由地址与路径全部匹配时才渲染页面)。

二级路由的定义:

二级路由的定义和一级路由一样(路由规则直接定义在组件内部,同时路由规则也充当了路由的渲染出口)。

注意:当某一个以及路由存在嵌套的子路由时,当前所在的以及路由规则就不能添加exact严格匹配,不然会无法渲染。

//路由定义代码演示组件懒加载以及组件渲染模式
//一级路由
import './App.css';

import React, { Component, lazy, Suspense } from 'react';
import { Route, Redirect, Switch } from 'react-router-dom'
/**
 * react 中通过 react-router 喝 react-router-dom 实现路由跳转,通过url地址的变化切换不同的内容
 * ------一级路由
 * 1.导入 Route 组件 (设置一级路由  path路径   component组件)  //需要使用Reuter去包裹路由组件 <BrowserRouter/>
 * 2.导入 Redirect 组件 (路由重定向  默认打开的页面)  要让路由变成严格匹配 exact (当页面路径与路由路径全部都相等时才匹配)
 * 3.通过设置 path="*"  component={NotFount}  来设置404页面
 * 4.Switch 组件  (Switch组件包裹Route组件,匹配到第一个就不再往下匹配)
 * ------二级路由
 * 1.在一级路由组件内定义二级路由
 * 2.二级路由组件内定义二级路由
 */

/**
 * 组件懒加载:
 * 在react中导入一个 lazy 函数 ,lazy函数返回一个函数,该函数返回一个React组件
 * 在导入一个 Suspense 组件,包裹懒加载的组件,当组件加载完成之前,显示一个加载中的组件  (在组件在懒加载创建的时候显示一些东西,不至于让他报错)
 * 
 * 懒加载对打包的影响:
 * 懒加载之后会打包都各自的js文件,如果没处理的话会默认打包到一个js文件中
 */

/**
 * 路由渲染的三种方式
 * component :组件渲染
 * component:传入一个回调函数,也需要将props传入进去
 * render:函数渲染 (函数返回一个组件) 不过他返回的是一个全新的组件,需要将props传入进去
 */

// 导入组件
// import Index from './view/Index.jsx'
// import Login from './view/Login.jsx'
// import NotFount from './view/NotFount.jsx'

// 懒加载组件
let Index = lazy(() => import('./view/Index.jsx'))
let Login = lazy(() => import('./view/Login.jsx'))
let NotFount = lazy(() => import('./view/NotFount.jsx'))



class App extends Component {
  render() {
    return (
      <div className='app'>
        <Suspense fallback={<div>加载中...</div>}> {/* fallback 属性设置加载中的组件 */}
          <Switch>
            {/*Route组件本身也充当一个路由出口  */}
            {/* 注意:当某个组件内存在嵌套子路由时,当前路由所在的路由规则就不能添加exact精确匹配属性,不然就会无法渲染 */}
            {/* <Route exact path="/index" component={Index} /> 一级路由 */}
            <Route path="/index" component={Index} />
            {/* <Route path="/login" component={Login} /> */}
            {/* <Route path="/login" component={(props) => {
              return <Login {...props} />
            }} /> */}
            <Route path="/login" render={(props) => {
              return <Login {...props} />
            }} />
            <Redirect exact from="/" to="/index" /> {/* 路由重定向  默认打开的页面  */}
            <Route path="*" component={NotFount} /> {/* 404页面  */}
          </Switch>
        </Suspense>

      </div>
    );
  }
}

export default App;

//二级路由
import React, { Component } from 'react'
import { NavLink, Route, Switch, Redirect } from 'react-router-dom'
import './Index.css'
// 导入组件
import Home from './Index/Home'
import Mine from './Index/Mine'
import NotFount from '../view/NotFount'

export default class Index extends Component {
    render() {
        return (
            <div className='index'>
                {/* 定义二级路由规则,也会充当渲染出口 */}
                <Switch>
                    <Route path="/index/home" component={Home} />
                    {/* 动态路由传参 */}
                    {/* <Route path="/index/mine/:id" component={Mine} /> */}
                    <Route path="/index/mine" component={Mine} />
                    <Redirect from="/index" to="/index/home" />
                    <Route path="*" component={NotFount} />
                </Switch>
            </div>
        )
    }
}

2.3 跳路由及路由传参

跳路由

声明式导航: Link(组件最终会渲染成一个a标签),NavLink(组件最终会渲染成一个标签,会自动维护active类的添加和删除)

编程式导航:

通过当前组件实例可以获取,histroy,location,match等属性。

histroy:可以使用其中的push,relpace,goBack,go等方法来跳路由。

location:pathname,search,state等属性来获取路由信息。

match:params,path,url等属性来获取路由信息。

//声明式导航
<NavLink to="/index/home">
     <span className='iconfont icon-home'></span>
     <div className="name">店铺</div>
</NavLink>
<NavLink to="/index/mine">
     <div className="name">个人中心</div>
</NavLink>

//编程式导航
 // 跳转到个人中心
    toMine() {
        // 通过js代码跳路由
        // this.props.history.push('/index/mine') 
        // this.props.history.push({ pathname: `/index/mine/${3}` })  //写成对象可以传参   动态路由传参

        //query传参
        // this.props.history.push({ pathname: '/index/mine', query: { id: 3 } })

        //state传参
        this.props.history.push({ pathname: '/index/mine', state: { id: 3 } })

        this.refs.mine.classList.add('active')
    }

<div ref='mine' className="active" onClick={() => { this.toMine() }}>个人中心</div>

路由传参以及接收路由参数

/**
 * -----路由传参
 * 1.动态路由传参
 * 路由地址中带有 :xx 这样得路由地址就是动态路由(刷新页面参数不会丢失,参数显示在地址栏)
 * <Route path="/index/home/:id" component={Home} />
 * 跳路由 this.props.history.push('/index/home/3')
 * 组件内通过 props.match.params.xx 接收参数
 * 
 * 2.通过query传参
 * 路由地址正常 (刷新页面参数会丢失,参数不显示在地址栏)
 * 跳路由 props.history.push({ pathname: '/index/mine', query: { id: 3 } })
 * 组件内通过 props.location.query 获取参数 
 * 
 * 3.通过state传参
 * 路由地址正常 (刷新页面参数不会丢失,参数不显示在地址栏)
 * 跳路由 props.history.push({ pathname: '/index/mine', state: { id: 3 } })
 * 组件内通过 props.location.state 获取参数
 */
 
 //跳路由
   // 跳转到个人中心
    toMine() {
        // 通过js代码跳路由
        // this.props.history.push('/index/mine') 
        // this.props.history.push({ pathname: `/index/mine/${3}` })  //写成对象可以传参   动态路由传参

        //query传参
        // this.props.history.push({ pathname: '/index/mine', query: { id: 3 } })

        //state传参
        this.props.history.push({ pathname: '/index/mine', state: { id: 3 } })

        this.refs.mine.classList.add('active')
    }
    
//接收参数
  componentDidMount() {

        // 接收动态路由传递过来的参数
        console.log(this.props)
        // console.log(this.props.match.params.id)


        // 接收query传递的参数
        // console.log(this.props.location.query)

        // 接收state传递的参数
        console.log(this.props.location.state)
    }

注意: 任何一个组件,只要是通过路由规则匹配渲染的,都会嵌入 history,location,match等属性。如果没有可以使用 withRouter 高阶组件去处理一下就可以了。

3.路由的使用 v6版本

3.1 安装

npm i react-router-dom@6

3.2 定义路由规则

v5跟v6区别:

  • 在v6中Route 中引入组件,不再使用component属性,而是element属性,而且组件不能跟以前一样直接放进去,得放成标签的样子。
  • 在v6中Switch 成了 Routes。
  • 在v6中Redirect 成了 Navigate。
  • 在v6中Routes 内部只能嵌套 Route或 Fragment。
  • 在v6中路由定义 支持嵌套定义,不像v5,要分散在各个组件中去定义。
  • 在v6中,如果某个路由规则下有二级路由,则要在该路由规则对应的组件下放置一个 Outlet 组件,作为路由渲染的出口。

v6路由也需要用路由管理组件去包裹路由规则。

import './App.css';
import React from 'react';

import { Route, Navigate, Routes } from 'react-router-dom'
// 在v6中
// 在v6中Route 中引入组件,不再使用component属性,而是element属性
// 在v6中Switch 成了 Routes
// 在v6中Redirect 成了 Navigate
// 在v6中Routes 内部只能嵌套 Route或 Fragment
// 在v6中路由定义 支持嵌套定义,不想v5,要分散在各个组件中去定义
// 在v6中,如果某个路由规则下有二级路由,则要在改路由规则对应的组件下放置一个 Outlet 组件,作为路由渲染的出口 

// 一级路由组件
import Index from './components/Index'
import Login from './components/Login'

// 二级路由组件
import Home from './components/Index/Home'
import Mine from './components/Index/Mine'

function App(props) {
    return (
        <div className='app'>
            {/* 定义路由规则 */}
            <Routes>
                {/* 一级路由 */}
                <Route path='/index' element={<Index />} >
                    {/* 定义二级路由 */}
                    <Route path='/index/home' element={<Home />} />
                    <Route path='/index/mine' element={<Mine />} />
                    {/* 重定向*/}
                    <Route path='/index' element={<Navigate to='/index/home' />} />
                    {/* 404 */}
                    <Route path='*' element={<div>404</div>} />
                </Route>
                <Route path='login' element={<Login />} />
                {/* 重定向 由于Routes组件中之恶能写Route或者Fragment组件,所以重定向要这么写 */}
                <Route path='/' element={<Navigate to='/index' />} />
                {/* 404 */}
                <Route path='*' element={<div>404</div>} />
            </Routes>
        </div>
    );
}

export default App;

3.3 定义配置式路由(跟Vue形式的路由相似)

在项目目录下新建一个router文件夹,去定义路由规则并导出。然后再组件里面去生成路由规则。

需要调用路由库提供的 useRouters 钩子函数,生成具体的路由规则。

// index.js
// 定义react路由规则
import {Navigate} from 'react-router-dom'
// 一级路由组件
import Index from '../components/Index'
import Login from '../components/Login'

// 二级路由组件
import Home from '../components/Index/Home'
import Mine from '../components/Index/Mine'


const routers = [
    {
        path:'/index',
        element:<Index/>,
        children:[
            {
                path:'/index/home',
                element:<Home/>
            },
            {
                path:'/index/mine',
                element:<Mine/>
            },
            {
                path:'/index',
                element:<Navigate to='/index/home'/>
            },
            {
                path:'*',
                element:<div>404</div>
            }

        ]
    },
    {
        path:'/login',
        element:<Login/>
    },
    {
        path:'/',
        element:<Navigate to='/index'/>
    },
    {
        path:'*',
        element:<div>404</div>
    }
]

// 导出路由规则,然后再组件里面去生成路由规则
export default routers;

//app.jsx
import './App.css';
import React from 'react';

// 如果路由规则通过外部js文件定义,需要导入到App组件中,统一生成具体的路由规则
// 需要调用路由库提供的 useRouters 钩子函数,生成具体的路由规则。生成的路由规则是一个数组,数组中的每一项是一个对象,对象中包含path和element属性

import { useRoutes } from 'react-router-dom'

// 导入外部的路由规则
import routes from './router/index'

function App(props) {
    return (
        <div className='app'>
            {/* 生成路由规则 */}
            {
                useRoutes(routes)
            }
        </div>
    );
}

export default App;

3.4 跳路由

跳路由方式依旧是两种:

  1. 通过路由库提供的Link和NavLink组件去跳路由。
  2. 通过路由库提供的useNavigator钩子函数去创建一个路由对象去跳路由。

因为函数式组件没有this,他的props里面也没有 history,location,match这三个对象,多以不能跟类组件中那样去跳。

v6路由中组件的porps默认为空,不再注入history,location,match等属性。 v6路由提供了useNavigatot钩子函数用来跳路由(需要在路由库中来导入)。

import React, { useState } from 'react';
import { Outlet, useNavigate, useLocation, useMatch } from 'react-router-dom';
import { TabBar } from 'antd-mobile'
import { AppOutline, UserOutline } from 'antd-mobile-icons'

// 跳路由方式依旧是两种

function index(props) {

    const navigator = useNavigate()
    const [activeKey, setActiveKey] = useState('/index/home')
    const tabs = [
        {
            key: '/index/home',
            title: '首页',
            icon: <AppOutline />,
        },
        {
            key: '/index/mine',
            title: '我的',
            icon: <UserOutline />,
        },
    ]

    // const [activeKey, setActiveKey] = useState('/index/home');

    const handleChange = (key) => {
        setActiveKey(key);
        // setActiveKey(key);
        // 跳路由
        // v6路由中组件的porps默认为空,不在注入history,location,match等属性
        // v6路由提供了Navigator方法用来跳路由
        navigator(key)
    }
    return (
        <div>
            index
            {/* 充当路由组件的占位出口 */}
            <Outlet />
            <TabBar style={{ position: 'fixed', bottom: 5, left: 0, right: 0 }} activeKey={activeKey} onChange={handleChange}>
                {tabs.map(item => (
                    <TabBar.Item key={item.key} icon={item.icon} title={item.title} />
                ))}
            </TabBar>
        </div>
    );
}

export default index;

3.5 路由传参及接收路由参数

  1. 动态路由传参: 参数会显示在地址栏,刷新页面参数不会消失。需要在定义路由地址的时候添加一个占位符。

    //定义路由地址
    {
         path:'/detail/:id',
         element:<Detail/>
     },
     
     //跳路由
     import React from 'react';
     import { useNavigate } from 'react-router-dom'
    
     function Home(props) {
         // 创建路由对象
         const navigator = useNavigate()
         //跳转到商品详情
         const toDetail = () => {
             // 传递路由参数
             navigator('/detail/888')
         }
         return (
             <div>
                 home组件
                 <button onClick={toDetail}>跳转到商品详情</button>
             </div>
         );
     }
    
     export default Home;
     
    

    接收路由参数: 需要从react-router-dom中导出一个useParams钩子函数去接收参数。

     import React, { useEffect } from 'react';
     import { useParams } from 'react-router-dom'
     // 在这个文件里面接收路由参数
     function Detail(props) {
         // 接收;路由参数
         let { id } = useParams()
         return (
             <div className='detail'>
                 详情页
                 <p>接收到的路由参数:{id}</p>
             </div>
         );
     }
    
     export default Detail;
    
  2. 查询字符串方式: 参数显示在地址栏,刷新页面参数不会丢失。

    import React from 'react';
    import { useNavigate } from 'react-router-dom'
    
    function Home(props) {
        // 创建路由对象
        const navigator = useNavigate()
        //跳转到商品详情
        const toDetail = () => {
            // 传递路由参数
            // 查询字符串形式传参
            navigator('/detail?id=888')
            // 还有一种写法
            navigator({
                pathname: '/detail',
                search: '?id=888'
            })
        }
        return (
            <div>
                home组件
                <button onClick={toDetail}>跳转到商品详情</button>
            </div>
        );
    }
    
    export default Home;
    

    接收参数: 需要从react-router-dom中导出一个useSearchParams钩子函数去接收参数。

    import React, { useEffect } from 'react';
    import {useSearchParams } from 'react-router-dom'
    // 在这个文件里面接收路由参数
    function Detail(props) {
        // 接收;路由参数
        // 使用useSearchParams() 接收查询在字符出串路由参数
        let [params, setParams] = useSearchParams()
        console.log(params)
        return (
            <div className='detail'>
                详情页
                {/* <p>接收到的路由参数:{id}</p> */}
                <p>接收到的路由参数:{params.get('id')}</p>
            </div>
        );
    
    }
    
    export default Detail;
    
  3. state传参: 参数不会携带在地址栏,刷新参数不会丢失。

import React from 'react';
import { useNavigate } from 'react-router-dom'

function Home(props) {
  // 创建路由对象
  const navigator = useNavigate()
  //跳转到商品详情
  const toDetail = () => {
      // 传递路由参数

      //state传参
      navigator('/detail', { state: { id: 888 } })
  }
  return (
      <div>
          home组件
          <button onClick={toDetail}>跳转到商品详情</button>
      </div>
  );
}

export default Home;

接收参数: 需要从react-router-dom中导出一个useLocation钩子函数去接收参数。

import React, { useEffect } from 'react';
 import { useLocation} from 'react-router-dom'
 // 在这个文件里面接收路由参数
 function Detail(props) {
     // 接收;路由参数
     //使用useLocation接收state传递过来的参数
     let location = useLocation()
     console.log(location)
     return (
         <div className='detail'>
             详情页
             <p>接收到的路由参数:{location.state.id}</p> 
         </div>
     );

 }
 export default Detail;

3.6 路由懒加载

react项目在打包完成后发现 chunk.js 文件比较大,导致首屏加载需要的时间比较长,可以使用路由懒加载的方式来进行优化。

在react中要实现路由懒加载,需要从react中导入 lazy函数和Suspense组件,去完成路由懒加载。实现路由懒加载之后,只有当组件需要被渲染时才会实时去加载组件,另外,当组件被加载过以后,下一次会直接加载。

    //使用Suspense组件包裹路由规则
    import './App.css';
    import React, { Suspense } from 'react';
    // 如果路由规则通过外部js文件定义,需要导入到App组件中,统一生成具体的路由规则
    // 需要调用路由库提供的 useRouters 钩子函数,生成具体的路由规则。生成的路由规则是一个数组,数组中的每一项是一个对象,对象中包含path和element属性

    import { useRoutes } from 'react-router-dom'

    // 导入外部的路由规则
    import routes from './router/index'

    function App(props) {
        // console.log(useRoutes(routes))
        return (
            <div className='app'>
                {/* 生成路由规则 */}
                {/* 通过Suspense包裹路由规则 */}
                <Suspense fallback={<div>...</div>}>
                    {
                        useRoutes(routes)
                    }
                </Suspense>


            </div>
        );
    }

    export default App;
    
    
    //使用lazy函数懒加载导入组件
    // 定义react路由规则

    import {lazy,Suspense} from 'react'
    import {Navigate} from 'react-router-dom'
    // 一级路由组件
    // import Index2 from '../components/Index2'
    // import Login from '../components/Login'
    // import Detail from '../components/Detail'

    // 二级路由组件
    // import Home from '../components/Index/Home'
    // import Mine from '../components/Index/Mine'


    // 懒加载
    const Index2 = lazy(()=>import('../components/Index2'))
    const Login = lazy(()=>import('../components/Login'))
    const Detail = lazy(()=>import('../components/Detail'))

    const Home = lazy(()=>import('../components/Index/Home'))
    const Mine = lazy(()=>import('../components/Index/Mine'))


    const routers = [
        {
            path:'/index',
            element:<Index2/>,
            children:[
                {
                    path:'/index/home',
                    element:<Home/>
                },
                {
                    path:'/index/mine',
                    element:<Mine/>
                },
                {
                    path:'/index',
                    element:<Navigate to='/index/home'/>
                },
                {
                    path:'*',
                    element:<div>404</div>
                }

            ]
        },
        {
            path:'/login',
            element:<Login/>
        },
        {
            // path:'/detail/:id',
            path:'/detail',
            element:<Detail/>
        },
        {
            path:'/',
            element:<Navigate to='/index'/>
        },
        {
            path:'*',
            element:<div>404</div>
        }
    ]

    // 导出路由规则,然后再组件里面去生成路由规则
    export default routers;

3.7 解决路由懒加载导致页面抖动问题

由于是通过Suspense包裹所有的路由规则和lazy来实现的懒加载,所以当加载路由的时候,所有组件都会重新渲染。解决方法就是给每一个二级路由组件和三级路由组件去包裹Suspense组件。

import {Suspense} from 'react'
 
//封装一个函数式组件(高阶组件),来处理,也可以去加一些别的逻辑

function AuthComponent({children}){
    return (
        <Suspense>
            {Children}
        </Suspense>
    )
}


//使用封装的组件包裹路由渲染的组件
const routers = [
    {
        path:'/index',
        element : <AuthComponent> <Home/> </AuthComponent>  //会将Home放在props的children里面
    }
]