手写react-router-dom

需要实现的功能

在这里插入图片描述

  • 路由模式:hashbrowser
  • 组件:HashRouterBrowserRouterRouteSwitchRedirectLink
  • 当前路由信息:路径pathname,参数query
  • 路由跳转:pushreplacegogoBack

react-router-dom使用方式

// 路由配置
import React from 'react'
// react-router-dom
import { HashRouter as Router, Route, Redirect, Switch } from 'react-router-dom'
// 手写react-router-dom
// import { HashRouter as Router, Route, Redirect, Switch } from '@/plugins/my-router-dom'
import Login from '../pages/login'
import User from '../pages/system/user/index'
import Home from '../pages/test/home'
import Classify from '../pages/test/classify'
import Car from '../pages/test/car'
import Mine from '../pages/test/mine'

function Routes () {
  return (
    <Router>
      <Switch>
        <Route exact path="/login" component={Login}></Route>
        <Route exact path="/user" component={User}></Route>
        <Route exact path="/home" component={Home}></Route>
        <Route exact path="/classify" component={Classify}></Route>
        <Route exact path="/car" component={Car}></Route>
        <Route exact path="/mine" component={Mine}></Route>
        <Redirect to="/home"></Redirect>
      </Switch>
    </Router>
  )
}

export default Routes
// 页面中使用
// 事件跳转的方式
goOther = (path) => {
  this.props.history.push(path)
}

--------
// 链接跳转的方式
import {Link} from '@/plugins/my-router-dom'
<Link to="/classify?id=111">分类</Link>

目录结构

├── my-router-dom
	├── index.js
	├── context.js
	├── history.js
	├── HashRouter.js
	├── BrowserRouter.js
	├── Route.js
	├── Redirect.js
	├── Switch.js
	├── Link.js
	└── listen.js

实现过程

context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

为了使每个组件都能拿到 当前路由信息location路由跳转的方法history,这里使用了Context
在context.js文件下创建一个context对象

  • Provider是包装组件,它的作用是接收一个 value 属性,传递给消费组件
  • Consumer是消费组件,它的作用接收当前的 context 值,返回一个 React 节点
// /my-router-dom/context.js
import React from 'react'

let {Provider, Consumer} = React.createContext()
export {Provider, Consumer}

history

这个文件写一些路由跳转的方法,路由跳转有 hashbrowser 两种模式,每种模式都有 pushreplacegogoBack 4中跳转方式,由于不同模式的跳转 api 不同,因此需要分开写。

  • 其中hash模式使用的是 window.location.hash = ‘/home’ 和 window.location.replace(’/#/home’) 方法
  • browser模式使用的是 window.history.pushState(null, ‘’, ‘/home’) 和 window.history.pushState(null, ‘’, ‘/home’) 方法

另外,由于在使用时传入的参数可能时一个字符串,也有可能时一个对象,因此需要对参数做类型判断,以hash模式的push方法为例

const url = require('url')
// hash
function hashPush (path) {
  if (typeof path === 'string') {
    window.location.hash = path.indexOf('/') === 0 ? path : ('/' + path)
    return
  }
  if (typeof path === 'object') {
    let obj = {
      pathname: path.path || '/',
      query: path.query || {}
    }
    const formatUrl = url.format(obj)
    window.location.hash = formatUrl.indexOf('/') === 0 ? formatUrl : ('/' + formatUrl)
  }
}
  • 如果参数是字符串,则先判断参数是否以 / 开头,如果时直接跳转,不是就先加上 / 再跳转
  • 如果参数时对象,使用node自带的url模块快速解析和生成跳转路径,然后判断是否以 / 开头再跳转

下面是history.js文件完整的代码,目前仅支持路由传参,不支持state隐式传参

// /my-router-dom/history.js
const url = require('url')
// hash
function hashPush (path) {
  if (typeof path === 'string') {
    window.location.hash = path.indexOf('/') === 0 ? path : ('/' + path)
    return
  }
  if (typeof path === 'object') {
    let obj = {
      pathname: path.path || '/',
      query: path.query || {}
    }
    const formatUrl = url.format(obj)
    window.location.hash = formatUrl.indexOf('/') === 0 ? formatUrl : ('/' + formatUrl)
  }
}

function hashReplace (path) {
  if (typeof path === 'string') {
    window.location.replace(path.indexOf('/') === 0 ? ('/#' + path) : ('/#/' + path))
    return
  }
  if (typeof path === 'object') {
    let obj = {
      pathname: path.path || '/',
      query: path.query || {}
    }
    const formatUrl = url.format(obj)
    window.location.replace(formatUrl.indexOf('/') === 0 ? ('/#' + formatUrl) : ('/#/' + formatUrl))
  }
}

// browser
function browserPush (path) {
  if (typeof path === 'string') {
    window.history.pushState(null, '', path)
    return
  }
  if (typeof path === 'object') {
    let obj = {
      pathname: path.path || '/',
      query: path.query || {}
    }
    const formatUrl = url.format(obj)
    window.history.pushState(null, '', formatUrl)
  }
}
function browserReplace (path) {
  if (typeof path === 'string') {
    window.history.replaceState(null, '', path)
    return
  }
  if (typeof path === 'object') {
    let obj = {
      pathname: path.path || '/',
      query: path.query || {}
    }
    const formatUrl = url.format(obj)
    window.history.replaceState(null, '', formatUrl)
  }
}


function go (num) {
  window.history.go(num)
}
function back () {
  go(-1)
}

// hash
export const hashHistory = {
  push: hashPush,
  replace: hashReplace,
  go: go,
  back: back
}
// browser
export const browserHistory = {
  push: browserPush,
  replace: browserReplace,
  go: go,
  back: back
}

HashRouter

这个文件就是一个react组件,只不过它需要作为 context 的包装组件,负责往消费组件传递 路由信息路由跳转方法
另外还有一个重要的作用就是需要监听路由hash的变化,然后实时更新传递的路由信息

// /my-router-dom/HashRouter.js
import React, { Component } from 'react'
import { Provider } from './context'
import { hashHistory } from './history'
const url = require('url')

class HashRouter extends Component {
  constructor () {
    super()
    this.state = {
      pathName: window.location.hash.slice(1) || '/',
    }
  }

  componentDidMount () {
    // 如果没用hash
    window.location.hash = window.location.hash || '/'
    window.addEventListener('hashchange', () => {
      this.setState({
        pathName: window.location.hash.slice(1) || '/'
      })
    })
    this.setState({
      pathName: window.location.hash.slice(1) || '/'
    })
  }

  render () {
    let value = {
      type: 'HashRouter',
      history: hashHistory,
      location: Object.assign({
        pathname: '/'
      }, url.parse(this.state.pathName, true))
    }
    return (
      <Provider value={value}>
        {this.props.children}
      </Provider>
    )
  }
}

export default HashRouter
  • 因为首次进入路由可能没有hash,所以首次加载的时候给路由加上hash ----> window.location.hash = window.location.hash || ‘/’
  • 添加hash变化的监听方法 hashchange,当路由hash变化,立即更新路由信息
  • 根据路由pathName,使用url模块转成路由信息的对象
  • 最后传递给消费组件3个参数,type:路由模式,history:hash路由跳转方法,location:当前路由信息
  • this.props.children 是所有的子组件

BrowserRouter

BrowserRouter 和 HashRouter 作用类似,只不过HashRouter传递的是hash模式,而BrowserRouter传递的是browser模式

// /my-router-dom/BrowserRouter.js
import React, { Component } from 'react'
import { Provider } from './context'
import { browserHistory } from './history'
const url = require('url')

class BrowserRouter extends Component {
  constructor () {
    super()
    this.state = {
      pathName: window.location.pathname || '/',
    }
  }

  componentDidMount () {
    this.setPathname()
    window.addEventListener('popstate', () => {
      this.setPathname()
    })
    window.addEventListener('pushState', () => {
      this.setPathname()
    })
    window.addEventListener('replaceState', () => {
      this.setPathname()
    })
  }
  setPathname = () => {
    this.setState({
      pathName: window.location.pathname || '/'
    })
  }

  render () {
    let value = {
      type: 'BrowserRouter',
      history: browserHistory,
      location: Object.assign({
        pathname: '/'
      }, url.parse(this.state.pathName, true))
    }
    return (
      <Provider value={value}>
        {this.props.children}
      </Provider>
    )
  }
}

export default BrowserRouter
  • 由于使用了h5 的history api,使用 popstate 监听方法可以监听浏览器前进,后退的点击事件,但是 window.history.pushState() 和 window.history.replaceState() 的事件却不能监听到,所以这里需要自己给window.history 添加 两个监听方法

在这里插入图片描述
在这里插入图片描述
listen.js 文件中写入如下方法,然后在 index.js 文件中引入

// /my-router-dom/listen.js
(function () {
  if (typeof window === undefined) {
    return
  }
  var _wr = function (type) {
    var orig = window.history[type];
    return function () {
      var rv = orig.apply(this, arguments);
      var e = new Event(type);
      e.arguments = arguments;
      window.dispatchEvent(e);
      return rv;
    }
  }
  window.history.pushState = _wr('pushState');
  window.history.replaceState = _wr('replaceState');
})();
// /my-router-dom/index.js
import './util/listen'
  • 然后就可以在 componentDidMount 生命周期中添加,事件监听的方法,当路由变化,立即更新pathName,并将pathName转成url对象
  • 最后传递给消费组件3个参数,type:路由模式,history:browser路由跳转方法,location:当前路由信息

Route

Route.js 文件也是一个react组件,作为消费组件,它的作用主要是根据路由返回正确匹配的组件。

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

class Route extends Component {
  render () {
    console.log('Route render')
    return (
      <Consumer>
        {
          (state) => {
            let { path, component: View } = this.props
            if (path === state.location.pathname) {
              return <View {...state}></View>
            }
            return null
          }
        }
      </Consumer>
    )
  }
}

export default Route
  • state 是包装组件传递给消费组件的 value,即 type:路由模式,history:路由跳转方法,location:当前路由信息
  • props 是父组件传递过来的参数
  • 当父组件传递过来的path 与 当前路由信息的pathname 一致,就返回 component

Link

这个实现起来比较简单,这个组件需要返回一个a标签,但是不能使用a标签默认跳转的方式,需要阻止默认事件,然后事件跳转的方式

扫描二维码关注公众号,回复: 11184276 查看本文章
// /my-router-dom/Link.js
import React, { Component } from 'react'
import { Consumer } from './context'

class Link extends Component {
  render () {
    console.log('Link render')
    return (
      <Consumer>
        {
          (state) => {
            let to = this.props.to || '/'
            to = to.indexOf('/') === 0 ? to : '/' + to
            return <a {...this.props} href={state.type === 'BrowserRouter' ? to : '/#' + to} onClick={(e) => {
              if (e && e.preventDefault) {
                e.preventDefault()
              } else {
                window.event.returnValue = false
              }
              state.history.push(to)
            }}>{this.props.children}</a>
          }
        }
      </Consumer>
    )
  }
}

export default Link

Redirect

如果没有匹配到正确的路由,有时候我们需要做重定向。

// /my-router-dom/Redirect.js
import React, { Component } from 'react'
import { Consumer } from './context'

class Redirect extends Component {
  render () {
    console.log('Redirect render')
    return (
      <Consumer>
        {
          (state) => {
            state.history.push(this.props.to)
            return null
          }
        }
      </Consumer>
    )
  }
}

export default Redirect
  • 这个组件不需要返回任何的东西,它的作用是直接使用事件跳转的方式跳转到目标路由。

但是在使用的时候,我们会发现一个问题,无论有没有匹配到正确的路由,最后都会重定向到 Redirect 组件,原因就是在路由配置文件中,它会依次加载组件,无论是否匹配成功,它都会执行到最后一个组件,因此如果 Redirect 组件放到最后,就会重定向

因此,我们需要 Switch 组件

Switch

这个组件也是消费组件,但是它会用来包裹 Route 组件。Route 组件做循环,这个组件的作用是只会成功匹配一次,如果正确匹配了,就不会继续执行下面的 组件。

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

class Switch extends Component {
  render () {
    console.log('Switch render')
    return (
      <Consumer>
        {
          (state) => {
            for (let i = 0; i < this.props.children.length; i++) {
              const path = (this.props.children[i].props && this.props.children[i].props.path) || '/'
              const reg = new RegExp('^' + path)
              if (reg.test(state.location.pathname)) {
                return this.props.children[i]
              }
            }
            return null
          }
        }
      </Consumer>
    )
  }
}

export default Switch
  • this.props.children 是所有的子组件
  • 使用正则来匹配路由,这里不能直接使用 === 判断,因为如果使用 === 判断,就不会执行最后 Redirect 组件了
  • 最后返回正确匹配的路由

end

手写 react-router-dom 只是为了更好地理解 react-router-dom 的使用,个人写的只是简单地实现路由的跳转,事实上有些地方写的可能不是很正确,也不是很完善。如果有什么建议或者想法,欢迎pr。
最后附上源码:github

原创文章 16 获赞 12 访问量 2677

猜你喜欢

转载自blog.csdn.net/weixin_44775548/article/details/102951604