React路由之HashRoute的實現原理


在上一篇中我們實現了BrowserRoute,這篇我們繼續實現HashRouter。

本文的核心功能:

  • HashRouter
  • Route
  • Link
  • NavLink
  • Switch
  • Redirect
  • withRouter

1、HashRouter

HashRouter只是一個容器,並沒有DOM結構,它渲染的就是它的子組件,並向下層傳遞location,代表當前的路徑,當hash值發生變化的時候會通過hashchange捕獲變化,並給pathname重新賦值,看一個具體的例子:
import React from "react"; import ReactDOM from "react-dom"; import {HashRouter as Router, Route} from 'react-router-dom' //路由庫
import Home from './components/Home' import User from './components/User' import Profile from './components/Profile' ReactDOM.render( <Router>
    <Route exact path="/" component={Home}></Route>
    <Route path="/user" component={User}></Route>
    <Route path="/profile" component={Profile}></Route>
  </Router>,document.getElementById('root')
)
  • Router是路由容器,
  • Route是路由規則,一個Route代表一個路由規則
  • path 代表路徑
  • component代表要渲染的組件

HashRouter的實現如下:

import React, {useState} from 'react' import RouterContext from './RouterContext'
/** * HashRouter只是一個容器,並沒有DOM結構,它渲染的就是它的子組件,並向下層傳遞location */ export default class HashRouter extends React.Component { state = { location: { pathname: window.location.hash.slice(1), // #/user
 state:window.history.state } } //組件掛載完成之后,根據hash改變pathname的值
 componentDidMount(){ window.addEventListener('hashchange',event =>{ this.setState({ ...this.state, location: { ...this.state.location, pathname:window.location.hash.slice(1) } }) }) window.location.hash = window.location.hash || '/' //如果沒有hash值就給一個默認值
 } render() { //渲染子組件 如果子組件里面嵌套着二級三級路由需要通過上下文Context取
        let routerValue = { location: this.state.location } return ( // this.props.children
            <RouterContext.Provider value={routerValue}> {this.props.children} </RouterContext.Provider>
 ) } }

因為HashRouter渲染的是它的子組件,那么子組件里面有可能嵌套着二級三級路由,這個時候就需要上下文Context來讀取嵌套的值,需要創建一個Context

import React from 'react'
//創建一個上下文
let context = React.createContext() export default context

在上面的HashRouter里面引入

2.Route

文章開始的例子中我們說過,route代表一條路由規則,path代表此規則的路徑, component代表要渲染的組件,如果說通過Context傳下來的路徑location.pathname與當前屬性中的路徑path相匹配就進行渲染。

import React from 'react' import RouterContext from './RouterContext' import pathToRegexp from 'path-to-regexp'
 export default class Route extends React.Component{ static contextType = RouterContext //拿到的就是 this.context.location.pathname
 render() { let {path, component: RouteComeponent, exact} = this.props let pathname = this.context.location.pathname; //拿到地址欄路徑
        let paramName = [] let regexp = pathToRegexp(path, paramName, {end:exact}) //進行匹配
        if(regexp.test(pathname)) { //渲染
            return <RouteComeponent></RouteComeponent>
        }else { return null } } }

3.Link超鏈接

點擊某個鏈接跳轉到指定頁面。實例:

import React from "react"; import ReactDOM from "react-dom"; import {HashRouter as Router, Route,Link} from 'react-router-dom' //路由庫
import Home from './components/Home' import User from './components/User' import Profile from './components/Profile'
/** * Router是路由容器 * Route是路由規則,一個Route代表一個路由規則 * path 代表路徑 component代表要渲染的組件 */ ReactDOM.render( <Router>
    <Link to="/">home</Link>
    <Link to="/user">user</Link>
    <Link to="/profile">profile</Link>
  </Router>,document.getElementById('root')
)

渲染結構:

 

 

 可以看到它的渲染結構就是一個a鏈接,href就是屬性to對應的值,所以可以這么實現link方法:

import React from 'react'
function Link (props) { return ( <a href="{`#${props.to}`}">{props.children}</a>
 ) }

但是這種方法只適用於hash路由,如果不是hash路由,就需要通過上下文拿到一個history對象的一個push方法,實現路徑的跳轉。

import React from 'react' import RouterContext from './RouterContext'
function Link (props) { return ( <RouterContext.Consumer> { routerValue => ( <a href="{`#${props.to}`}" onClick={() => routerValue.history.push(props.to)}>{props.children}</a>
 ) } </RouterContext.Consumer>
 ) }

同時需要修改HashRouter傳過來的實參

修改前:

 

 修改后:

 

 接下來實現動態路由和二級路由

 

4.Switch

switch是為了解決route的唯一渲染,保證路由只渲染一個路徑。

如果配置了<Switch>

<Router history={history}>
    <Switch>
        <Route path='/home' render={()=>(<div>首頁</div>)}/>
        <Route path='/home' component={()=>(<div>首頁</div>)}/>
    </Switch>
</Router>

 

 如果沒有配置:

<Router history={history}>
    <Route path='/home' render={()=>(<div>首頁</div>)}/>
    <Route path='/home' render={()=>(<div>首頁</div>)}/>
</Router>

 

 實現Switch:

 

import React, { useContext } from 'react' import RouterContext from './RouterContext' import pathToRegexp from 'path-to-regexp'

/** * switch 的作用是負責子組件的匹配,只會渲染第一個匹配上的子組件 * useContext 獲取上下文對象的一種方式 * @param {*} props */ export default function (props) { let routerContext = useContext(RouterContext) //拿到上下文中傳過來的location
    let children = props.children children = Array.isArray(children) ? children : [children] //判斷是否是數組,如果不是就包裝成數組
    let pathname = routerContext.location.pathname //對子組件進行匹配
    for(let i = 0; i < children.length; i++) { let child = children[i] //child是一個react元素它的返回值是一個虛擬dom {type:Route,props:{exact,path,component}}
        let {path='/',component,exact=false} = child.props //取出對應的屬性
        let regexp = pathToRegexp(path, [], {end: exact}) let matched = pathname.match(regexp) //若匹配進行渲染
        if(matched) { return child } } //若不匹配就返回null
    return null }

拿到上下文中傳過來的location,然后取出pathname。再對它的子組件進行遍歷,如果子組件的path屬性和當前上下文中傳過來的pathname屬性相匹配就進行渲染,若不匹配就返回null。

5.Redirect

重定向,當所有都不匹配的時候會重定向到新的頁面,就是改變path值驅動頁面重新渲染。

例子:

import React from "react"; import ReactDOM from "react-dom"; import { HashRouter as Router, Route, Link, Switch, Redirect } from "react-router-dom"; //路由庫
import Home from "./components/Home"; import User from "./components/User"; import Profile from "./components/Profile"; /** * Router是路由容器 * Route是路由規則,一個Route代表一個路由規則 * path 代表路徑 component代表要渲染的組件 */ ReactDOM.render( <Router>
    <Link to="/">home</Link>
    <Link to="/user">user</Link>
    <Link to="/profile">profile</Link>
    <Switch>
      <Route exact path="/" component={Home}></Route>
      <Route path="/user" component={User}></Route>
      <Route path="/user" component={User}></Route>
      <Route path="/profile" component={Profile}></Route>
      <Redirect to="/"></Redirect>
      <Redirect from="/home" to="/"></Redirect> 
    </Switch>
  </Router>,
  document.getElementById("root") );

第一個Redireact是當location.pathname與上面所有的path屬性不相等的時候會重定向到path='/'的頁面

第二個Redireact是當location.pathname為/home的時候,重定向到path='/'的頁面

實現Redireact:

import React, { useContext } from 'react' import RouterContext from './RouterContext' export default function (props) { let routerContext = useContext(RouterContext) //當Redirect元素的props.from屬性和當前location.pathname屬性相等時或者from屬性不存在時就直接跳轉到to
    if(!props.from || props.from === routerContext.location.pathname) { routerContext.history.push(props.to) } return null }

6.NavLink

NavLink的原理和Link的原理一樣,但是它多了一個功能,如果說to的路徑和當前地址欄的路徑匹配的話就給當前路徑對應的元素增加了一個active類名,表示當前路徑是激活狀態,這個功能在菜單欄中經常用到,點擊某個菜單按鈕給它一個高亮的顏色,表示是激活的狀態。

具體實現:

NavLink.js

import React from 'react' import {Route,Redirect} from 'react-router-dom' export default function (props) { let {to, exact, children} = props return ( //使用Route來渲染的好處就是可以在children函數里面通過props.match是否有值來判斷是否匹配
        <Route path={typeof to === 'string' ? to : to.pathname} children={ //children也是一個函數,不管路徑匹配與否,都會渲染
        routerProps => <Link className={routerProps.match && (!exact || (exact && routerProps.match.isExact)) ? 'active' : ''} to={to}>{children}</Link>
        }></Route>
 ) } //組件渲染的三種方式: /** * render component都有一個共同的特點就是Route的path要跟路徑匹配的話才會渲染,不然不會進行渲染 * children也是一個函數,不管路徑匹配與否,都會渲染 */

NavLink.css

.active { background-color: aqua; color: red ; }

 7.withRouter

withRouter是一個高階組件,它的作用是將一個自定義組件包裹進Route里面, 然后react-router的三個對象history, location, match就會被放進這個組件的props屬性中。從而實現自定義組件的路由跳轉。具體看下面的例子:

ReactDOM.render( <Router>
    <NavHeader></NavHeader>
    <ul>
      <li><NavLink exact={true} to="/">home</NavLink></li>
      <li><NavLink to="/user">user</NavLink></li>
      <li><NavLink to="/profile">profile</NavLink></li>
    </ul>
  </Router>,
  document.getElementById("root") );

Navheader.js

import React from 'react' export default function() { return ( <div className="navbar-heading">
            <div onClick={()=> props.history.push('/')} //點擊的時候跳轉到首頁
            className="navbar-brand">返回首頁</div>
        </div>
 ) }

希望點擊這個組件,跳轉到首頁,但是報錯:沒有history屬性。 因為這個組件不是通過route渲染出來的,拿不到history屬性。這個時候用withRouter,他會返回一個被route處理過后的組件,這個新組件拿到了history屬性,從而實現了跳轉。

import React from 'react' import withRouter from 'react-router-dom'
 function Navheader() { return ( <div className="navbar-heading">
            <div onClick={()=> props.history.push('/')} //點擊的時候跳轉到首頁
            className="navbar-brand">返回首頁</div>
        </div>
 ) } export default withRouter(Navheader)

withRouter的具體實現:

import React from 'react' import {Route} from 'react-router-dom' export default function (OldComponent) { return props => ( <Route component={OldComponent}></Route>
 ) }

大概實現就是這樣的,但是上面我們並沒有用到props,因為沒有從父組件傳參過去,下面看一個傳參數的例子;

ReactDOM.render( <Router>
    <NavHeader title="返回首頁"></NavHeader>
    <ul>
      <li><NavLink exact={true} to="/">home</NavLink></li>
      <li><NavLink to="/user">user</NavLink></li>
      <li><NavLink to="/profile">profile</NavLink></li>
    </ul>
  </Router>,
  document.getElementById("root") );

withRouter.js

import React from 'react' import {Route} from 'react-router-dom' export default function (OldComponent) { //props={title:返回首頁} 
    //routeProps={location,history,match}
    return props => ( // <Route component={OldComponent}></Route>
        <Route render={ routeProps => <OldComponent {...props} {...routeProps} />
        } />
 ) }

通過render的方式渲染就可以將參數傳給里面的NavHeader組件,其實也可以用children的方式也能渲染,因為此處並沒有傳path屬性,所以不管匹配與否都會進行渲染。(直接將render改成children就行)

import React from 'react' import withRouter from 'react-router-dom'
 function Navheader(props) { return ( <div className="navbar-heading">
            <div onClick={()=> props.history.push('/')} //點擊的時候跳轉到首頁
            className="navbar-brand">{props.title}</div>
        </div>
 ) } export default withRouter(Navheader)

 

 推薦博文:

https://segmentfault.com/a/1190000014313428

https://www.cnblogs.com/sunLemon/p/9020153.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM