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

posted @ 2020-04-22 20:33  leahtao  阅读(3523)  评论(0编辑  收藏  举报