ReactRouter

zfh大约 12 分钟约 3622 字...

前端路由的原理

前端路由是如何做到 URL 和内容进行映射呢?监听 URL 的改变

  • URL 发生变化,同时不引起页面的刷新有两个办法:

    • 通过 URLhash 改变 URL
    • 通过 HTML5 中的 history 模式修改 URL
  • 当监听到URL发生变化时,我们可以通过自己判断当前的 URL,决定到底渲染什么样的内

hash

URL hash 也就是锚点(#), 可以通过 location.hash 来改变 href, 但是页面不发生刷新

注意:

  • hash 的优势就是兼容性更好,在老版 IE(最低兼容到 IE3)中都可以运行

  • 但是缺陷是有一个#,显得不像一个真实的路径,或者说有点丑 💩

  <body>
        <div id="app">
            <a href="#/home">主页</a>
            <a href="#/about">关于</a>
            <div id="router-view"></div>
        </div>
        <script>
            const routerViewEl = document.getElementById('router-view')
            window.addEventListener('hashchange', () => {
                console.log(location.hash)
                switch (location.hash) {
                    case '#/home':
                        routerViewEl.innerHTML = '首页'
                        break
                    case '#/about':
                        routerViewEl.innerHTML = '关于'
                        break
                    default:
                        routerViewEl.innerHTML = ''
                }
            })
        </script>
    </body>

HTML5 的 history

history 接口是HTML5新增的, 它有六种模式改变 URL 而不刷新页面:

  • replaceState:替换原来的路径
  • pushState:使用新的路径
  • popState:路径的回退
  • go:向前或向后改变路径
  • forward:向前改变路径
  • back:向后改变路径
<div id="app">
  <a href="/home">首页</a>
  <a href="/about">关于</a>

  <div class="router-view"></div>
</div>

<script>
  // 1.获取router-view的DOM
  const routerViewEl = document.getElementsByClassName('router-view')[0]

  // 获取所有的a元素, 自己来监听a元素的改变
  const aEls = document.getElementsByTagName('a')
  for (let el of aEls) {
    el.addEventListener('click', (e) => {
      e.preventDefault()
      const href = el.getAttribute('href')
      history.pushState({}, '', href)
      urlChange()
    })
  }

  // 调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。
  // 无论是浏览器的前进还是后退都会触发这个popstate事件,所以只能起到一个监听页面变化的作用。
  window.addEventListener('popstate', urlChange)

  // 监听URL的改变
  function urlChange() {
    switch (location.pathname) {
      case '/home':
        routerViewEl.innerHTML = '首页'
        break
      case '/about':
        routerViewEl.innerHTML = '关于'
        break
      default:
        routerViewEl.innerHTML = ''
    }
  }
</script>

React-Router V5

React Router 的版本 4 开始,路由不再集中在一个包中进行管理了:

  • react-routerrouter 的核心部分代码
  • react-router-dom 是用于浏览器的
  • react-router-native 是用于原生应用的

目前我们使用的 React Router 版本是@5.2.0 的版本

安装 react-router-dom 会自动帮助我们安装react-router的依赖:yarn add react-router-dom

基本使用

react-router 最主要的 API 是给我们提供的一些组件:

  • BrowserRouter HashRouter
    • BrowserRouter 使用history模式
    • HashRouter 使用 hash 模式
  • Link 不太常用,通常在项目中使用编程式导航
    • 路径的跳转是使用 Link 组件,最终会被渲染成 a 元素
    • to 属性:Link 中最重要的属性,用于设置跳转到的路径
  • Route
    • Route 用于路径的匹配
    • path 属性:用于设置匹配到的路径
    • component 属性:设置匹配到路径后,渲染的组件
    • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
import { Component } from 'react'
import { BrowserRouter, Link, Route } from 'react-router-dom'
import Home from './pages/home'
import About from './pages/about'

class App extends Component {
  render() {
    return (
      <div>
        <BrowserRouter>
          <Link to='/'>首页</Link>
          <Link to="/about">关于</Link>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
        </BrowserRouter>
      </div>
    )
  }
}

export default App

Switch

我们来看下面的路由规则:

  • 当我们匹配到某一个路径时,我们会发现有一些问题
  • 比如/about/xxxx 路径匹配到的同时,/about 也被匹配到了,且*总是匹配到
    <Route exact path='/' component={Home}/>
                    <Route path='/about/xxxx' component={Aboutx}/>
                    <Route path='/about' component={About}/>
                    <Route path="*"><NoMatch /></Route>          

原因是什么呢?默认情况下,react-router 中只要是路径被匹配到的 Route 对应的组件都会被渲染

但是实际开发中,我们往往希望有一种排他的思想:只要匹配到了第一个,那么后面的就不应该继续匹配了;这个时候我们可以使用 Switch 来将所有的 Route 进行包裹即可

<Switch>
    <Route exact path='/' component={Home}/>
    <Route path='/about' component={About}/>
    <Route path="*"><NoMatch /></Route>   
</Switch>

Redirect

Redirect 用于路由的重定向,当这个组件出现时,就会执行跳转到对应的 to 路径中:

我们这里使用这个的一个案例:

用户跳转到 User 界面,但是在 User 界面有一个 isLogin 用于记录用户是否登录:

  • true:显示用户的名称

  • false:直接重定向到登录界面

// user.jsx
import { Component } from 'react'
import { Redirect } from 'react-router-dom'

class User extends Component {
  constructor() {
    super()
    this.state = {
      isLogin: false,
    }
  }

  render() {
    return (
      <div>
        {this.state.isLogin ? <h2>用户:Frank</h2> : <Redirect to={'/login'} />}
      </div>
    )
  }
}

export default User

404

必须放在最后一个,且必须使用Switch包裹

    <Route path="*" component={404}></Route>

路由嵌套

在开发中,路由之间是存在嵌套关系的

这里我们假设 about 页面中有两个页面内容:

  • 商品列表和消息列表
  • 点击不同的链接可以跳转到不同的地方,显示不同的内容
import { Component } from 'react'
import { Link, Switch, Route } from 'react-router-dom'

function GoodList() {
  return (
    <ul>
      {[1, 2, 3].map((item) => {
        return <li>{`商品${item}`}</li>
      })}
    </ul>
  )
}

function NewsList() {
  return (
    <ul>
      {[1, 2, 3].map((item) => {
        return <li>{`消息${item}`}</li>
      })}
    </ul>
  )
}

class Abouts extends Component {
  render() {
    return (
      <div>
        <Link to={'/about'}>商品列表</Link>
        <Link to={'/about/news'}>消息列表</Link>

        <Switch>
          <Route exact path={'/about'} component={GoodList} />
          <Route path={'/about/news'} component={NewsList} />
        </Switch>
      </div>
    )
  }
}

export default Abouts

编程式导航

通过 JavaScript 代码进行跳转有一个前提:必须获取到 history 对象

如何可以获取到 history 的对象呢?两种方式:

  • 如果该组件是通过路由直接跳转过来的,那么可以直接获取 historylocationmatch 对象

提示

  • history.push 这个方法会向 history 栈里面添加一条新记录,这个时候用户点击浏览器的回退按钮可以回到之前的路径。

  • history.go 这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)

  • history.replace 跟 history.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。

import { Component } from 'react'
import { NavLink, Switch, Route } from 'react-router-dom'

import styled from 'styled-components'

const AboutWrapper = styled.div`
  .about-active {
    color: orange;
  }
`

function JoinUs() {
  return <div>加入我们:zfhblog.top</div>
}

function GoodList() {
  return (
    <ul>
      {[1, 2, 3].map((item) => {
        return <li key={item}>{`商品${item}`}</li>
      })}
    </ul>
  )
}

function NewsList() {
  return (
    <ul>
      {[1, 2, 3].map((item) => {
        return <li key={item}>{`消息${item}`}</li>
      })}
    </ul>
  )
}

class Abouts extends Component {
  render() {
    return (
      <div>
        <AboutWrapper>
          <NavLink exact to={'/about'} activeClassName={'about-active'}>
            商品列表
          </NavLink>
          <NavLink to={'/about/news'} activeClassName={'about-active'}>
            消息列表
          </NavLink>
          <button
            onClick={() => {
              this.joinUs()
            }}
          >
            加入我们
          </button>
        </AboutWrapper>
        <Switch>
          <Route exact path={'/about'} component={GoodList} />
          <Route path={'/about/news'} component={NewsList} />
          <Route path={'/about/join'} component={JoinUs} />
        </Switch>
      </div>
    )
  }

  joinUs() {
    this.props.history.push('/about/join')
  }

  componentDidMount() {
    console.log(this.props.history)
  }
}

export default Abouts
  • 如果该组件是一个普通渲染的组件,那么不可以直接获取 history、location、match 对象

那么如果普通的组件也希望获取对应的对象属性应该怎么做呢?

前面我们学习过高阶组件,可以在组件中添加想要的属性;react-router 也是通过高阶组件为我们的组件添加相关的属性的:

如果我们希望在 App 组件中获取到 history 对象,必须满足以下两个条件:

  • App 组件必须包裹在 Router 组件之内
  • App 组件使用 withRouter 高阶组件包裹
// app.js
import { Component } from 'react'
import { NavLink, Route, withRouter } from 'react-router-dom'
import Home from './pages/home'
import Abouts from './pages/abouts'
import Order from './pages/order'
import styled from 'styled-components'

const NavLinkWrapper = styled.div`
  width: 100vw;
  height: 100vh;
  display: flex;

  .link {
    padding-top: 50px;
    width: 100px;
    background-color: #f1f1f1;
    display: flex;
    align-items: center;
    flex-direction: column;
  }

  a {
    text-decoration: none;
    margin-right: 20px;
    font-size: 20px;
  }

  a.link-active {
    color: red;
  }
`

class App extends Component {
  render() {
    return (
      <NavLinkWrapper>
        <div className="link">
          <NavLink exact to="/" activeClassName={'link-active'}>
            首页
          </NavLink>
          <NavLink to="/about" activeClassName={'link-active'}>
            关于
          </NavLink>
          <button
            to="/order"
            onClick={() => {
              this.toOrder()
            }}
          >
            订单
          </button>
        </div>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={Abouts} />
        <Route path="/order" component={Order} />
      </NavLinkWrapper>
    )
  }

  toOrder() {
    this.props.history.push('/order')
  }
}

export default withRouter(App)
// index.js
ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
)

参数传递

动态路由 Param

动态路由的概念指的是路由中的路径并不会固定:

比如/detail 的 path 对应一个组件 Detail。如果我们将 path 在 Route 匹配时写成/detail/:id,那么 /detail/abc、/detail/123 都可以匹配到该 Route,并且进行显示这个匹配规则,我们就称之为动态路由。通常情况下,使用动态路由可以为路由传递参数

import {Component} from 'react';
import {NavLink, Route, withRouter} from "react-router-dom";
import Home from "./pages/home";
import Abouts from "./pages/abouts";
import Order from "./pages/order";
import styled from "styled-components";

const NavLinkWrapper = styled.div`
  width: 100vw;
  height: 100vh;
  display: flex;

  .link {
    padding-top: 50px;
    width: 100px;
    background-color: #f1f1f1;
    display: flex;
    align-items: center;
    flex-direction: column;
  }

  a {
    text-decoration: none;
    margin-right: 20px;
    font-size: 20px;
  }

  a.link-active {
    color: red;
  }
`

class App extends Component {
    render() {
        return (
            <NavLinkWrapper>
                <div className='link'>
                    <NavLink exact to='/' activeClassName={'link-active'}>首页</NavLink>
                    <NavLink to='/about' activeClassName={'link-active'}>关于</NavLink>
                    <button to='/order' onClick={() => {
                        this.toOrder()
                    }}>订单
                    </button>
                </div>
                <Route exact path='/' component={Home}/>
                <Route path='/about' component={Abouts}/>
                <Route path='/order/:id' component={Order}/>
            </NavLinkWrapper>
        );
    }

    toOrder() {
        this.props.history.push('/order/123')
    }
}

export default withRouter(App);
// ------------------------------------------
// order.jsx
// ------------------------------------------
import {Component} from 'react';

class Order extends Component {
    render() {
        return (
            <div>
                <h2>订单:{this.props.match.params.id}</h2>
            </div>
        );
    }
}

export default Order;

查询参数 Query

// app.js
import { Component } from 'react'
import { NavLink, Route, withRouter } from 'react-router-dom'
import Home from './pages/home'
import Abouts from './pages/abouts'
import Order2 from './pages/order2'
import styled from 'styled-components'

const NavLinkWrapper = styled.div`
  width: 100vw;
  height: 100vh;
  display: flex;

  .link {
    padding-top: 50px;
    width: 100px;
    background-color: #f1f1f1;
    display: flex;
    align-items: center;
    flex-direction: column;
  }

  a {
    text-decoration: none;
    margin-right: 20px;
    font-size: 20px;
  }

  a.link-active {
    color: red;
  }
`

class App extends Component {
  render() {
    return (
      <NavLinkWrapper>
        <div className="link">
          <NavLink exact to="/" activeClassName={'link-active'}>
            首页
          </NavLink>
          <NavLink to="/about" activeClassName={'link-active'}>
            关于
          </NavLink>
          <button
            to="/order"
            onClick={() => {
              this.toOrder()
            }}
          >
            订单
          </button>
        </div>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={Abouts} />
        <Route path="/order" component={Order2} />
      </NavLinkWrapper>
    )
  }

  toOrder() {
    this.props.history.push('/order?id=123&name=frank&test=2&test2=1231231')
  }
}

export default withRouter(App)
import { Component } from 'react'

class Order2 extends Component {
  render() {
    return (
      <div>
        <h2>订单:{this.props.match.params.id}</h2>
      </div>
    )
  }

  componentDidMount() {
    let query = {}
    const search = this.props.location.search.split('&')
    search[0] = search[0].split('?')[1]
    search.forEach((item) => {
      const kv = item.split('=')
      query[kv[0]] = kv[1]
    })
    console.log(query)
  }
}

export default Order2

路由配置

import {BrowserRouter, Link, Route, Switch} from "react-router-dom";
import  {Component} from "react";
// 一些用于展示用的组件
// ==========================================================
class Home extends Component {
    render() {
        return (
            <div>
                <h2>主页</h2>
            </div>
        );
    }
}

function GoodList() {
    return (
        <ul>
            {
                [1, 2, 3].map(item => {
                    return <li key={item}>{`商品${item}`}</li>
                })
            }
        </ul>
    )
}


function NewsList() {
    return (
        <ul>
            {
                [1, 2, 3].map(item => {
                    return <li key={item}>{`消息${item}`}</li>
                })
            }
        </ul>
    )
}

class About extends Component {
    render() {
        return (
            <div>
                <h2>关于</h2>
                <Link to='/about'>商品</Link>
                <Link to='/about/news'>新闻</Link>
                <Switch>
                    {this.props.routes.map((route, i) => (
                        <RouteWithSubRoutes key={i} {...route} />
                    ))}
                </Switch>
            </div>
        );
    }
}
// ==========================================================
// 路由配置数组 模板代码抽离
const routes = [
    {
        path: "/",
        exact: true,
        component: Home
    },
    {
        path: "/about",
        component: About,
        routes:[
            {
                path: "/about",
                exact:true,
                component: GoodList
            },
            {
                path: "/about/news",
                component: NewsList
            }
        ]
    }
]
// 核心路由组件转换函数  render-props:https://zh-hans.reactjs.org/docs/render-props.html#gatsby-focus-wrapper
function RouteWithSubRoutes(route) {
    return (
        <Route
            path={route.path}
            render={props => {
            return  <route.component {...props} routes={route.routes} />
            }
            }
        />
    );
}

export default function App() {
    return (
        <BrowserRouter>
            <div>
                <Link to='/'>首页</Link>
                <Link to='/about'>关于</Link>
                <Switch>
                    {
                        routes.map((route, i) => {
                                return <RouteWithSubRoutes key={i} {...route} />
                            }
                        )
                    }
                </Switch>
            </div>
        </BrowserRouter>
    )
}

React-Router V6

基本使用

不同于vueRouter作为一个Vue插件进行注册使用,ReactRouter需要使用BrowserRouterhistory模式)或HashRouterhash 模式)对根App组件进行包裹使用

<HashRouter>
<App />
</HashRouter>

路由映射配置

Routes:包裹所有的Route,在其中匹配一个路由

v5使用的Switch组件,或者Route可以单独存在

Route:Route用于路径的匹配

  • path属性:用于设置匹配到的路径
  • element属性:设置匹配到路径后,渲染的组件 v5使用的是component属性
  • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件 v6不再支持该属性,自动精准匹配

路由配置和跳转

通常路径的跳转是使用Link组件,最终会被渲染成a元素

to属性:Link中最重要的属性,用于设置跳转到的路径

import {Link, Route, Routes} from "react-router-dom";
import About from "./About"; 
import Home from "./Home";

function App(props) {
    return (
        <div>
            <header>
                <Link to={'/'}>首页</Link>
                <Link to={'/about'}>关于</Link>
            </header>
            <div className="main">
                <Routes>
                    <Route  path='/' element={<Home/>}></Route>
                    <Route path='/about' element={<About/>}></Route>
                </Routes>
            </div>
        </div>
    );
}

export default App;

navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中。

例如判断当前用户登录状态,未登录跳转到登录页:

function Home(props) {
    const {isLogin,changeLoginStatus}=useState(false)
    return (
        <div>
            {
                isLogin?'这是首页':<Navigate to={'/login'}></Navigate>
            }
        </div>
    );
}

404页面配置

设置这样一个Route,当前面的路由都没有匹配上就会匹配这个路由:

                <Routes>
                    <Route  path='/' element={<Home/>}></Route>
                    <Route path='/about' element={<About/>}></Route>
                    <Route path='*' element={<NotFound />}></Route>
                </Routes>



 

路由嵌套

让父路由包裹子路由:

                <Routes>
                    <Route path='/' element={<Navigate to={'/home'}/>}></Route>
                    <Route path={'/home'} element={<Home/>}>
                        {/*子路由匹配时,/home默认跳转到'/home/shop' 防止页面空白*/}
                        <Route path='/home' element={<Navigate to={'/home/shop'}/>}></Route>
                        <Route path={'/home/shop'} element={<Cart/>}></Route>
                        <Route path={'/home/news'} element={<New/>}></Route>
                    </Route>
                    <Route path='/about' element={<About/>}>
                    </Route>
                    <Route path='/login' element={<Login/>}></Route>
                    <Route path='*' element={<NotFound/>}></Route>
                </Routes>

home组件下:

            <div>
                <Link to={'/home/shop'}>商品列表</Link>
                <Link to={'/home/news'}>消息列表</Link>
            </div>
{/*用于在父路由元素中作为子路由的占位元素*/}
            <Outlet/>

编程式导航

函数组件

    const navigate=useNavigate()
    function goAbout(){
        navigate('/about')
    }
              <button onClick={goAbout}>
                    编程式导航去关于
                </button>

类组件

通过高阶组件实现

import {useNavigate} from "react-router-dom";


export default function (WrapperCompontent){
    return (props)=>{
        const navigator=useNavigate()
        return (
            <WrapperCompontent {...props} router={navigator}/>
        )
    }
}

路由参数传递

动态路由Params

生产

 <Route path={'/home/shop/details/:id'} element={<Details/>}></Route>
import {useNavigate} from "react-router-dom";

export default function Cart(props) {
    const navigator=useNavigate()
    return (
        <div>
            <ul>
                {
                    // 传递params id参数
                    [1,2,3].map(item=><li  onClick={()=>{navigator(`/home/shop/details/${item}`)}} key={item}>商品{item}</li>)
                }
            </ul>
        </div>
    );
}

消费

import {useParams} from "react-router-dom";

function Details(props) {
    let { id } = useParams();
    return (
        <div>
            商品ID为:{id}
        </div>
    );
}

export default Details;

查询参数Query

生产

   function goAbout() {
        navigate('/about?name=frank&age=19')
    }

消费

import { useSearchParams } from "react-router-dom";
function About(props) {
    let [searchParams, setSearchParams] = useSearchParams();
    const query=Object.fromEntries(searchParams)
    console.log(query)
    return (
        <div>
            这是关于页面
            我的姓名:
            {searchParams.get('name')}
            我的年龄:
            {searchParams.get('age')}
        </div>
    );
}

export default About;

路由配置文件

import About from "../About";
import Home from "../Home";
import NotFound from "../NotFound";
import Login from "../Login";
import New from '../New'
import Cart from "../Cart";
import Details from "../Details";
import {Navigate} from "react-router-dom";


export default [
    {
        path:'/',
        element:<Navigate to={'/home'}/>
    },
    {
        path:"/home",
        element: <Home />,
        children:[
            {
                path:'/home',
                element:<Navigate to={'/home/shop'}/>
            },
            {
                path:'/home/shop',
                element: <Cart/>
            },
            {
                path:'/home/news',
                element: <New />
            },
            {
                path:'/home/shop/details/:id',
                element: <Details/>
            },
        ],
    },
    {
        path:'/about',
        element: <About/>
    },
    {
        path:'/login',
        element: <Login/>
    },
    {
        path:"*",
        element: <NotFound/>
    }
]

使用

                {
                    useRoutes(routes)
                }

页面懒加载

const About=React.lazy(()=>import('../About'))
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
    <Suspense fallback={<div>Loading...</div>}>
        <BrowserRouter>
            <App/>
        </BrowserRouter>
    </Suspense>
)
上次编辑于:
本站勉强运行 小时
本站总访问量
網站計數器