React之路由篇

2,533 阅读3分钟

react-router-dom 中我们常用到的方法有:HashRouterBrowserRouterRouteLinkRedirectSwitch。但是它内部是怎么实现的呢?

1、概述

HashRouter 中存放着 locationhistoryRouteLinkRedirectSwitch 的实现都依赖于它。HashRouter是它们的根组件,存放着 context的数据

  • context
import React from 'react';
const context = React.createContext();
export default context;

2、HashRouter 的实现

historylocation 对象放在 context,子组件可以调用。 子组件调用push 方法,来改变 location.hash,同时监听 hashchange事件,完成相应的渲染。


import React, {Component} from 'react';
import Context from './context';
export default class HashRouter extends Component {
  // 定义一个初始化的state
  state = {
    location:{pathname:window.location.hash.slice(1)||'/'},
    state:null
  }
  componentDidMount(){
   // 核心就是监听 hashChange 事件
    window.addEventListener('hashchange',()=>{
      this.setState({
        location:{
          ...this.state.location,
          pathname:window.location.hash.slice(1), // #/a ---> /a
          state:this.locationState
        }
      })
    })
  }
  locationState = null;
  render(){
    let that = this;
    let value = {
      location:that.state.location,
      history:{
        push(to){// 定义一个history 对象,有一个push 方法用来跳转路径
          if(typeof to === 'object'){
            let {pathname,state} = to;
            that.locationState = state;
            window.location.hash = pathname;
          }else{
            that.locationState = null;
            window.location.hash = to;
          }
        }
      }
    }
    return (
      // 这里的value 属性是专门给context 提供的,就是存放共享数据的
      <Context.Provider value={value}>
      {this.props.children}
      </Context.Provider>
    )
  }
}

3、Route的实现


import React, {Component} from 'react';
import Context from './context';
import reg from 'path-to-regexp';
export default class Route extends Component{
  static contextType = Context;
  render(){
    let {pathname} = this.context.location;
    let {path='/',component:Component,exact=false} = this.props;
    let paramNames = [];
    // 用正则对路由进行匹配
    let regexp = reg(path,paramNames,{end:exact});
    let result = pathname.match(regexp);

    let props = {
      location:this.context.location,
      history:this.context.history,
    }
    if(result) {
      paramNames = paramNames.map(item=>item.name);
      let [url,...values] = result;
      let params = {};
      for(let i=0;i<paramNames.length;i++){
        params[paramNames[i]] = values[i];
      }
      props.match = {
        path,
        url,
        isExact: url===pathname,
        params
      }
      return (<Component {...props}/>);
    }
    return null;
  }
}

4、Switch 的实现

这里和 switch...case 的思想差不多,会和子组件进行匹配,一旦匹配到,就立刻返回匹配到的组件

import React, {Component} from 'react';
import Context from './context';
import PathToRegexp from 'path-to-regexp';
export default class Switch extends Component{
  static contextType = Context;
  
  render(){
    let {pathname} =this.context.location;

    let children = Array.isArray(this.props.children)?this.props.children:[this.props.children];

    for(let i=0;i<children.length;i++){
      let child = children[i];
      let {path='/',exact} = child.props;
      let paramNames = [];
      
      let regexp = PathToRegexp(path,paramNames,{end:exact});
      let result = pathname.match(regexp);
      
      if(result){
        return child;
      }
    }

    return null
  }
}

5、Redirect 的实现

它的实现很简单,它都用在 Switch 中 ,相当于switch..case 语法中的default。原理就是调用HashRouterhistorypush 方法,来改变location

import React, {Component} from 'react';
import Context from './context';

export default class Redirect extends Component{
  static contextType = Context
  componentDidMount(){
    this.context.history.push(this.props.to)
  }
  render(){
    return null;
  }
}
--------------------------------------------
使用:
<Redirect to="/home" from='/' /> 

6、Link 的实现

Linkreact中的作用是替代a 标签的,起到点击跳转的作用,它的原理和 Redirect类似,都是是调用 HashRouterhistorypush 方法。

import React, {Component} from 'react';
import Context from './context';
export default class Route extends Component{
  static contextType = Context
  
  render(){
    
    return (
      //这种也可以实现
      // <a to={`#{this.props.to}`}>{this.props.children}</a>
      <a {...this.props} onClick={()=>{this.context.history.push(this.props.to)}}>{this.props.children}</a>
    )
  }
}

7、menuLink

import React, {Component} from 'react';
import Route from "./Route";
import Link from './Link'

export default ({to, children}) => {
    // 如果匹配到了,就给当前组件一个激活状态的className
    return <Route path={to} children={props => (
        <li className={props.match ? "active" : ""}>
            <Link to={to}>{children}</Link>
        </li>
    )
    }/>
}

7、withRouter

一个普通的组件也想拥有 Route 上的属性和方法,使用 withRouter ,它的本质是一个高阶函数。

import React, {Component} from 'react';
import Route from './Route';
export default function(WrappedComponent){
  return ()=><Route component={WrappedComponent}/>
}

使用

import React from 'react';
import {withRouter} from '../react-router-dom'
 class NavHeader extends React.Component{
  render(){
    return (
      <div className="navbar-heading">
        // 未经过Route包装的 组件时没有 history 方法
        <div onClick={()=>this.props.history.push('/')}>XX科技</div>
      </div>
    )
  }
}
export default withRouter(NavHeader)

8、BrowserRouter 的实现方法

它的实现是靠监听 popstatepushstate 来完成的。

import React, {Component} from 'react';
import Context from './context';
// let pushstate = window.history.pushState;
export default class BrowserRouter extends Component {
  state = {
    location:{pathname:window.location.pathname||'/'},
    state:null
  }

  pushstate = window.history.pushState;

  componentDidMount(){
    // 因为原生的方法上没有onpushstate,所以需要改写 pushstate 方法
    window.history.pushState = (state,title,url) => {
      // 先调用原生的 方法
      this.pushstate.call(window.history,state,title,url)

      window.onpushstate.call(this,state,url)
    }

    window.onpopstate = (event) => {
      this.setState({
        location:{
          ...this.state.location,
          pathname:window.location.pathname,
          state:event.state
        }
      })
    }

    window.onpushstate =  (state,pathname) =>{
      this.setState({
        location:{
          ...this.state.location,
          pathname,
          state
        }
      })
    }
    
  }
  
  render(){
    let that = this;
    let value = {
      location:that.state.location,
      history:{
        push(to){// 定义一个history 对象,有一个push 方法用来跳转路径
          if(typeof to === 'object'){
            let {pathname,state} = to;
            window.history.pushState(state,'',pathname)
          }else{
            window.history.pushState(null,'',to)
          }
        }
      }
    }
    return (
      // 这里的value 属性是专门给context 提供的,就是存放共享数据的
      <Context.Provider value={value}>
      {this.props.children}
      </Context.Provider>
    )
  }
}

9、总结

本文使用 mdnice 排版