React-路由原理

638 阅读8分钟

路由是跟据不同的url地址展示不同的页面

后端路由

服务端解析url地址,发送数据,但是每一次切换都需要重新刷新页面,对用户体验不好

前端路由

用户在点击切换路由的时候不需要重新刷新页面,路由由前端维护,分为hash路由和history路由,带#号的为hash路由,hash路由兼容性比较好

history路由

H5新增了window.history.pushstate 和window.history.replaceState

hash路由

hashchange事件来监听url hash值的改变

react-router

架构

image.png

Route 拿到context 中的location 和history 给 component, 然后根据路径 path 和pathname 生出一个match 也传给component

如何实现

HashRouter

新建一个文件夹,react-router-dom, 在index.js 文件中导出 HashRouter,

export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter'

然后来写HashRouter.js 文件

实际上引用的还是Router组件,通过createHashHistory 创建hash的history对象,传给Router

// HashRouter.js
import { Router } from '../react-router'

import { createHashHistory } from 'history'

const HashRouter = (props) => {
  // 创建一个history对象,创建一个history对象,但是是使用hash实现
  let hashHistory = createHashHistory()
  return (
    <Router history={hashHistory}>
      {props.children}
    </Router>
  )

}

export default HashRouter

新建一个文件夹,react-router

在Router传值给Route的时候,一共传了三个对象,history,location, match, 通过context的方法传递给下面的Route。在这里我们写一个RouterContext.js文件,用于所有组件都可以引用到这个context

// RouterContext.js
import React from 'react'

const RouterContext = React.createContext();

export default RouterContext;

然后来实现Router

// Router.js

import React, {useState} from 'react'
import RouterContext from './RouterContext'

const Router = (props) => {

  const { history } = props; // 可能是hashHistory 可能是 browserHistory

  const [location, setLocation] = useState(history.location)

  // 监听历史对象中的路径变化,当路径发生变化后执行回调函数
  // 返回一个location 参数就是最新的对象
  
  history.listen(location => setLocation(location))
  

  // 里面有三个值
  const value = {
    history, // 这个里面的location和外面的是一样的
    // location 代表当前路径 url 地址
    // location: {pathname:'', state: undefined, search: '', hash:"", }
    // match: {
    //   isExact: false,
    //   path: '/',
    //   url:'/'
    // }
    location,
  }

  return (
    <RouterContext.Provider value = {value}>
      {props.children}
    </RouterContext.Provider>
  )
}

export default Router

最后我们来实现Route进行渲染不同的组件 这里判断传给route的path路径和location.pathname(当前地址中的url地址)匹不匹配,匹配就渲染组件

// Route.js

import React, {useContext} from 'react'
import RouterContext from './RouterContext'

const Route = (props) => {
  const { history, location, match } = useContext(RouterContext);
  const { path, component: RouterComponent, exact } = props;
  
  let renderElement = null;
  
  // 将history和location传给组件
  let renderProps = {
    history, 
    location
  }

  if (path === location.pathname) {
    renderElement =  < RouterComponent {...renderProps} />
  } 

  return renderElement
}
export default Route;

最后我们来实现 createHashHistory 这个方法, 用来创建出的 hash路由下面的 history 对象

在history文件夹下面新建一个createHashHistory.js 文件 用来实现history

// createHashHistory.js
const createHashHistory = () => {

  let action;
  let listenerList = [];
  let historyStack = [];
  let historyIndex = -1;
  let state;


  // 用于监听到location改变后修改location
  const listen = (handleLocation) => {
    // hadleLocation是一个改变location的函数
    listenerList.push(handleLocation)
    return () => {
      const index = listenerList.indexOf(handleLocation)
      listenerList.splice(index, 1);
    }

  }

  const handelHashChange = () => {
    // 当前的路径
    let pathname = window.location.hash.slice(1);
    // 将新的action和pathname赋值给histor.action
    Object.assign(history,{
      action,
      location: {pathname, state}
    })
    
    if (!action || action === 'PUSH') {
      historyStack[++historyIndex] = history.location
    } else if (action === 'REPLACE') {
      historyStack[historyIndex] = history.location
    }

    listenerList.forEach(handleLocation => handleLocation(history.location))
  }

  window.addEventListener('hashchange', handelHashChange)

  const push = (newPath, newState) => {
    action = 'PUSH';
    let pathname;
    if (typeof newPath === 'object') {
      state = newPath.state;
      pathname = newPath.pathname;
    } else {
      pathname = newPath;
      state = newState;
    }
    window.location.hash = pathname;
  }

  const replace = (newPath, nextState) => {
    action = 'REPLACE'
    let pathname;
    if (typeof newPath === 'object') {
      state = newPath.state;
      pathname = newPath.pathname
    } else {
      pathname = newPath;
      state = nextState
    }
    window.location.hash = pathname;
  } 

  const go = (n) => {
    action = 'POP'
    historyIndex += n;
    let newLocation = historyStack[historyIndex];
    state = newLocation.state;
    window.location.hash = newLocation.pathname;
  }

  const goBack = () => {
    go(-1)
  }

  const goForward = () => {
    go(1)
  }

  // 自己创建的一个对象,类似于window.history,用于hash路由,两者没有关系
  let history = {
    listen, //监听
    action,
    // 给location一个默认值
    location: {pathname:'/', state:undefined},
    go,
    goBack,
    goForward,
    push,
    replace,
  }


  // 当url输入的地址和当前地址是一样的时候,相当于刷新页面,这个时候是不会触发到hashchange事件的因为hash值没有变,所以需要判断一下,手动调用一下这个事件
  if (window.location.hash) {
    handelHashChange()
  } else {
    window.location.hash = "/"
  }

  // 返回自己创建的history对象
  return history

}

export default createHashHistory;

现在我们已经简单实现了,来看一下效果吧

hash路由实现.gif

historyRouter

和hashRouter一样,我们在react-router-dom文件夹新建一个BrowserRouter.js文件,然后导出

// BrowserRouter.js
import { Router } from '../react-router'
import createBrowserRouter from '../historya/createBrowserRouter'
const BrowserRouter = (props) => {
  let BrowserRouter = createBrowserRouter()
  return (
    <Router history={BrowserRouter}>
      {props.children}
    </Router>
  )
}
export default BrowserRouter;

然后来实现createBrowserRouter

createBrowserRouter和createhashRouter差别不多,就是关于历史栈是history里面内部维护的,当点击浏览器返回箭头或者调用go,goback的时候就会触发popstate,监听popstate来设置最新的history,更新渲染组件

// createBrowserRouter.js
const createBrowserRouter = () => {
  let action;
  let state;
  let listenerList = [];
  let globalHistory = window.history;

  const listen = (HandellisterLocation) => {
    listenerList.push(HandellisterLocation);
    return () => {
      const index = listenerList.indexOf(HandellisterLocation);
      listenerList.splice(index, 1)
    }
  }

  const changeState = (newLocaation) => {
    Object.assign(history, newLocaation);
    listenerList.forEach(HandellisterLocation => HandellisterLocation(history.location))
  }

  const push = (newPath, newState) => {
    action = 'PUSH'
    let pathname;
    if (typeof newPath === 'object') {
      pathname = newPath.pathname
      state = newPath.pathname
    } else {
      pathname = newPath;
      state = newState;
    }
    globalHistory.pushState(state, null, pathname)
    changeState({action, location: {pathname, state}})
  }

// 当回退或者前进的时候会执行,这个监听是浏览器自带的默认支持的
  window.onpopstate = () => {
    changeState({
      action:'POP',
      location: {pathname : window.location.pathname, state}
    })
  }

  const go = (n) => {
    window.history.go(n)
  }

  const goBack = () => {
    go(-1);
  }

  const goForward = () => {
    go(1)
  }

  let history = {
    action: "POP", // 当前最后一个动作是什么
    location: {pathname: window.location.pathname, state: window.location.state},
    go,
    push,
    goBack,
    goForward,
    listen,
    isBrowser:true
  }

  return history
}
export default createBrowserRouter;

然后我们将hashRouter换成BrowserRouter看下效果

现在history路由也可以愉快玩耍了(图片里面的hashRouter忘记改为historyRouter!😂)

history路由实现.gif

接下来我们来实现 Switch Redirect withRouter www.jianshu.com/p/8d3cf411a…

Switch 实现 (待修改)

在react-router中新建一个Switch.js文件

没有switch会全部匹配一遍,switch包裹后,只要有匹配的就不会继续匹配了


import React from 'react'
import RouterContext from './RouterContext'
import matchPath from './mathPath';

class Switch extends React.Component {
  static contextType = RouterContext

  render() {

    const {location} = this.context;
    let element, match;
    React.Children.forEach(this.props.children, child => {
      // 判断一个元素是不是一个合法的元素 
      if (!match && React.isValidElement(child)) { // 如果还没有任何一个元素匹配上
        element = child;
        match = matchPath(location.pathname, child.props)
      }
    })

    return match ? React.cloneElement(element, {computedMatch: match})  : null;

  }

}



export default Switch

Redirect 实现

在react-router中新建一个Redirect.js文件

<Redirect to="/Weclome" />

import { useContext } from 'react'
import LifeCycle from './LifeCycle'

import RouterContext from './RouterContext'

const Redirect = ({to}) => {

  const { history } = useContext(RouterContext)

  return (
    <LifeCycle onMount={() => history.push(to)}></LifeCycle>
  )

}

export default Redirect

withRouter 实现

在react-router中新建一个withRouter.js文件

withRouter 作用是将一个组件包裹进去,然后 history,location,match三个对象就会被放到这个组件的props属性中,如果我们某一个点击的不是router,但是在点击的时候需要跳转到一个页面,这个时候就要withRouter来做,例如现在的app,点击li的时候,拿不到histroy.push,所以需要withRouter包裹

// import { Route,
// } from 'react-router-dom'

import { HashRouter, Route } from './react-router-dom'
// import { HashRouter, Route, withRouter } from 'react-router-dom'

import Hello from './component/Hello'
import Weclome from './component/Weclome'
import Word from './component/Word'

const App = (props) => {

  console.log(props, 'App的props') // {} 用withRouter包裹后就有值

  const handlePush = () => {
    console.log(window.history, '---->handlePush')
    // 如果用window.history.pushState虽然路径变了,但是不会刷新组件,所以还是需要withRouter
    window.history.pushState(null, null, '/Weclome')
  }

  return (
    <>
        <li onClick={handlePush}>Hello</li>
        <li>word</li>
        <li>Weclome</li>
        <div>
        -----------------------------------
        </div>
        
            <Route path="/" component={Hello}></Route>
            <Route path="/Weclome" component={Weclome}></Route>
            <Route path="/Word" component={Word}></Route>
    </>
  )
}
export default App;

我们来实现一下withRouter 草鸡简单直接将context的值传给组件就可

import React from 'react'
import RouterContext from './RouterContext'

function withRouter(OldComponent) {
  return props => {
    return (
      <RouterContext.Consumer>
        {
          value => {
            return <OldComponent {...props} {...value} />
          }
        }
      </RouterContext.Consumer>
    )
  }
}

export default withRouter

实现效果

// app.js
import { HashRouter, Route, withRouter } from './react-router-dom'

// 中间省略

export default withRouter(App);  //包裹一下即可

LifeCycle 实现


import React, { useEffect } from 'react'


const LifeCycle = (props) => {

  useEffect(() => {

    props.onMount && props.onMount()

    return () => {
      props.onUnmount && props.onUnmount()
    }
  },[])

  return null;

}

export default LifeCycle

在react-router-dom 中还有 Link, NavLink 组件方法,我们也来实现以下

Link 实现

在react-router-dom中新建一个Link.js文件

import React, { useContext } from 'react'

import RouterContext from '../react-router/RouterContext'

const Link = (props) => {

  const {history} = useContext(RouterContext)

  const handleToLink = (e) => {
    e.preventDefault()
    history.push(props.to)
  }

  return (
    <a {...props} onClick={handleToLink}>
      {props.children}
    </a>
  )

}

export default Link

使用 <Link to="/Word"> to_word </Link>

NavLink 实现 (待修改)

在react-router-dom中新建一个NavLink.js文件

import React from 'react'

import Link from './Link'

import RouterContext, {} from '../react-router/RouterContext'


function NavLink(props) {
  let context = React.useContext(RouterContext);
  let {pathname} = context.location;
  const {
    to, // 匹配的路径
    className:classNameProp="", // 原生类名
    style: styleProp = {}, // 原始的行内样式对象
    activeClassName="",
    activeStyle={},
    children,
    exact
  } = props;
  // 匹配当前的路径和自己to路径 是否匹配
  let isActive = matchPath(pathname, {path: to, exact});
  let className = isActive? joinClassnames(classNameProp, activeClassName): classNameProp;
  let style = isActive ? {...styleProp, ...activeStyle}: classNameProp;
  let linkProps = {
    className,
    style,
    to, 
    children
  }
  return <Link {...linkProps} />;
}

function joinClassnames(...classNames) {
  // 把空的类名过滤掉
  return classNames.filter(c => c).join(' ');
}

export default NavLink;

直接判断路径想等需要优化,使用mathPath


import pathToRegexp from 'path-to-regexp';

const cache = {};

function compilePath(path, options) {
  let cacheKey = path + JSON.stringify(options)
  if (cache[cacheKey]) return cache[cacheKey]
  const keys = []; // 处理路径参数的
  const regexp = pathToRegexp(path, keys, options);
  let result = { keys, regexp }
  cache[cacheKey] = result;
  return result
}


/**
 * 
 * @param {*} pathname 浏览器真实的路径
 * @param {*} options  Route组件的属性
 * sensitive 是否大小写敏感
 */
function matchPath(pathname, options={}) {
  let { path="/", exact= false, strict= false, sensitive=fasle } = options;
  let { keys,  regexp } = compilePath(path, {end: exact, strict, sensitive})
  const match = regexp.exec(pathname)
  if (!match) return null;
  const [url, ...values] = match;
  const isExact = pathname === url;
  // 如果要求精确,但是不精确,也返回null
  if (exact && !isExact) return null;
  return {
    path, // Route里的path路径
    url, // 浏览器地址中的url
    isExact, // 是否精确匹配
    params:keys.reduce((memo, key, index)=>{
      memo[key.name] = values[index];
      return memo
    },{})
  }
}

export default matchPath

  • 指定一个route组件要渲染的内容有三种方式
  • 1。component属性,值是一个组件的类型,他不能写定义的逻辑
    1. render属性,他是一个函数,如果路径匹配的话,就要渲染它这个函数的返回值‘
  • 3, children属性,他也是一个函数
  • render 是匹配才渲染,不匹配不渲染
  • childern 不管匹配不匹配都渲染

// Route
import React from 'react'
import { Router } from 'react-router';
import RouterContext from './RouterContext'
import matchPath from './mathPath';

/**
 * 获取context的值
 * 匹配路由规则里面的path是否和当前地址中的url地址是不是相等,如果相等
 * 就渲染component,不相等就不渲染
 */l

class Route extends React.Component {
  static contextType = RouterContext

  render() {
    const { history, location } = this.context
    const { exact, path, component: RouteComponent, computedMatch, render } = this.props;
    
    // 在这里直接设置相等不合理,应该使用正则,这里用到 path-to-regexp库
    // const match = location.patchname === path;
    const match = computedMatch? computedMatch :  matchPath(location.patchname, this.props)

    let renderElement = null;
    let routeProps = {
      history,
      location,
    }
    if (match) { //路径匹配才会进来
      routeProps.match = match;
      if (RouteComponent) {
         renderElement = <RouteComponent {...routeProps} />;
      } else if (render) {
        renderElement = render(routeProps);
      }
     
    }
    return renderElement
  }

}

export default Route;

Prompt 实现(待修改)

import React from 'react'
import RouterContext from './RouterContext'
import lifeCyle from './Lifecycle'
import LifeCycle from './Lifecycle';

/**
 * 
 * @param {*} when 布尔值,表示要不要阻止跳转
 * 
 * @returns  message 函数,表示要阻止的时候显示什么提示信息 
 */
function Prompt({when, message}) {
  return (
    <RouterContext.Consumer>
      {
        value => {
          if (!when) return null;
          const block = value.history.block;
          return (
            <LifeCycle 
              onMount={self => { // self 是lifeCylcles的实例,this
                self.release = block(message)
              }}
              onUnmount={self => self.release()}
            />
          )
        }
      }
    </RouterContext.Consumer>
  )
}

export default Prompt


使用

<Prompt 
  when={this.state.isBlocking}
  message={
    location => `请问你是否真的要跳转到${location.pathname}?`
  }

/>