手写React-router

本文详细介绍了如何从零开始模仿官方react-router-dom,构建自己的HashRouter、Route、Switch、Link和Redirect组件,包括数据传递、正则匹配路由、组件间通信和导航。适合初学者理解React路由核心原理。
摘要由CSDN通过智能技术生成

手写React-router

    本文章将参照官方的react-router-dom,按照其部分组件最基本的功能,自己写一个react-router。其中包括HashRouter、Route、Switch、Link、Redirect,只实现了一些基本功能,如有错误以及不足之处,请评论留言指出。

1.基本环境搭建

    首先,我已经配置好了webpack环境,当然你也可以用脚手架创建一个项目,并安装好官方的react-router-dom,用于稍后的测试使用。我的项目文件目录如下:

在这里插入图片描述
其中,index.js为项目入口文件。代码如下:

//index.js

import ReactDOM from 'react-dom'
import React from 'react'

import App from './App'

ReactDOM.render(
    <App/>, document.getElementById('root')
)

我在App.js中先写几个普通的组件,分别是Home(主页)、About(关于)、User(用户中心)、UserProfile(用户配置)、NotFound用于稍后的使用。

//App.js
import React from 'react'

function Home() {
  return (
    <div className="Home">
      Home
    </div>
  )
}

function About() {
  return (
    <div className="About">
      About
    </div>
  )
}

function User(props) {
  return (
    <div className="User">
      User
    </div>
  )
}

function UserProfile() {
  return (
    <div className="UserProfile">
      UserProfile
    </div>
  )
}

function NotFound() {
  return (
    <div className="NotFound">
      NotFound
    </div>
  )
}

class App extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (
      <div className="App">
        <Home />
        <About />
        <User />
        <UserProfile />
      </div>
    )
  }
}

export default App

使用npm start 启动项目,运行正常。
在这里插入图片描述

2. HashRouter和数据传递

2.1 HashRouter组件的创建

    Router组件分为BrowserRouter和HashRouter。我们写的是HashRouter。

首先观察官方使用HashRouter的引入方式。

import { HashRouter as Router } from 'react-router-dom'

我们在src目录下新建一个react-router-dom的文件夹,在文件夹中新建一个index.js。这样的话,我们在引入的时候,路径可以直接引入到react-router-dom,从而省略index.js。

然后在react-router-dom文件夹中新建一个HashRouter.js。写一个HashRouter组件,在componentDidMount函数中,初始化一下当前页面的默认hash值。最后将组件导出。

在react-router-dom/index.js中引入HashRouter组件,并再次以对象的形式导出。

代码如下:

在这里插入图片描述

//react-router-dom/HashRouter.js
import React, { Component } from 'react'

class HashRouter extends Component {
  constructor(props){
    super(props)
    //初始化数据 获取当前的location
    this.state = {
      //使用slice是为了去掉 #
      location: window.location.pathname.slice(1) || '/'
    }
  }
  componentDidMount() {
    //设置默认的hash
    window.location.hash = window.location.hash || '/'
  }
  render() {
    return (
      <div>
        我是HashRouter组件
      </div>
    )
  }
}

export default HashRouter

现在,我们就可以在App.js中使用HashRouter这个组件了。在App.js中引入HashRouter并命名为Router,测试一下。

//App.js
import React from 'react'

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

function Home() {
  ...
}

function About() {
  ...
}

function User(props) {
  ...
}

function UserProfile() {
 ...
}

function NotFound() {
  ...
}

class App extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (
      <div className="App">
        <Router />
      </div>
    )
  }
}

export default App

结果页面中正常显示了HashRouter组件的内容,并携带默认hash值。
在这里插入图片描述

注: 下文中将HashRouter组件都称为Router组件。

2.2 Route组件的创建

我们知道Router组件是以双标签的形式存在的,并且将其做为外层组件,里边包含route组件和其他组件等。

我们在react-router-dom文件夹下新建一个Route.js,并导出。并且在react-router-dom的index.js中引入并再次导出,便于其他地方使用。代码如下:

//react-router-dom/Route.js
import React from 'react'

function Route() {
  return (
    <div>
      我是Route组件
    </div>
  )
}

export default Route
//react-router-dom/index.js
import HashRouter from './HashRouter'
import Route from './Route'

export {
  HashRouter,
  Route
}

Route组件基本创建完毕以后,我们测试一下。需要将HashRouter组件的内容修改为this.props.children。

//App.js
import React from 'react'
import { HashRouter as Router, Route } from './react-router-dom'
...
...
class App extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (
      <div className="App">
        <Router>
          <Route />
        </Router>
      </div>
    )
  }
}

export default App

//react-router-dom/HashRouter.js
import React, { Component } from 'react'

class HashRouter extends Component {
  constructor(props){
    super(props)
    //初始化数据 获取当前的location
    this.state = {
      //使用slice是为了去掉 #
      location: window.location.pathname.slice(1) || '/'
    }
  }
  componentDidMount() {
    //设置默认的hash
    window.location.hash = window.location.hash || '/'
  }
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
}

export default HashRouter

结果页面显示出了Route组件的内容,证明是没有问题的。
在这里插入图片描述

2.3 利用Context进行数据传递

首先,使用官方react-router,写一个最基本的路由,渲染一个组件。观察一下该组件中的this.props。

在目录中新建一个App2.js,用于做测试。

import React from 'react'
import { HashRouter as Router, Route } from 'react-router-dom'

function Home(props) {
  console.log(props);
  return (
    <div className="Home">
      Home
    </div>
  )
}

class App2 extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (
      <div className="App">
        <Router>
          <Route path="/home" component={Home} />
        </Router>
      </div>
    )
  }
}

export default App2

组件里面打印props内容如下:
在这里插入图片描述
这里有三个对象,history、location、match,这三个对象都是我们需要关注的。首先,我们将会利用到history的push方法跳转路由。会用到location中的pathname属性,记录当前的路径名。会用到match对象中的isExact精确匹配路由,和params对象,获取路由传递的参数。

由官方的react-router测试,我们可知,通过Router组件包裹Route组件后,会给Route组件渲染的component组件传递以上的数据。

查阅react官方文档,我们找到了React.createContext这个API,用它来传递数据。

在react-router-dom文件夹下新建一个context.js。通过调用React.createContext()方法, 获取Provider(用来提供数据)和Consumer(同来消费数据)。

//react-router-dom/context.js
const myContext = React.createContext()

const { Provider, Consumer } = myContext

export {
  Provider,//提供数据
  Consumer//消费数据
}

因为HashRouter是最外层组件,包裹了route组件。所以要在Router组件提供数据,在Route组件消费数据。

( 注:此时Router组件就是HashRouter组件。)

我们在Router中使用Provider来提供数据。通过value的形式将数据传递出去。

并且利用onhashchange事件来监听hash值的变化。在这里onhashchange事件要用箭头函数,this才指的是组件实例,才能调用setState方法。

//react-router-dom/HashRouter.js
import React, { Component } from 'react'
import { Provider } from './context'

class HashRouter extends Component {
  constructor(props){
    super(props)
    //初始化数据 获取当前的location
    this.state = {
      //使用slice是为了去掉 #
      location: {
        pathname: window.location.hash.slice(1) || '/'
      }
    }
  }
  componentDidMount() {
    //设置默认的hash
    window.location.hash = window.location.hash || '/'
    //监听hash值的变化
    window.onhashchange = () => {
      this.setState({
        location: {
          pathname: window.location.hash.slice(1)
        }
      })
    }
  }
  render() {
    const value = {
      location: this.state.location,
      //history这里先放一个空对象
      history:{}
    }
    //Provider提供数据  value就是传递出去的数据
    return (<Provider value={value}>{this.props.children}</Provider>)
  }
}

export default HashRouter

数据提供出去以后,我们需要在Route组件中来获取消费数据。通过查阅官方文档,我们查到了Consumer的使用方法。
在这里插入图片描述
注意: 这里需要函数组件来使用Consumer,类组件会报错。具体的报错解决方法,不是本文章的主要内容,这里没有做具体的深究。

在Route.js中使用Consumer,并在标签中写一对花括号表示我们要写js代码。然后写一个箭头函数用来消费Provider提供的数据,value是形参,也就是Provider传递过来的数据。

//react-router-dom/Route.js
import React from 'react'
import { Consumer } from './context'

function Route() {
  return (
    <Consumer>
      {
        //定义一个函数,来消费Provider提供的数据
        value => {
          console.log('value::', value);
          return null
        }
      }
    </Consumer>
  )
}

export default Route

在这里插入图片描述

此时,我们在Router中用Provider提供数据,数据中记录的是当前页面的hash值。在Route中用Consumer消费数据。并且绑定了一个监听页面hash值变化的事件。

此时我们能做到:

如果地址栏路径后的hash值变化==>

onhashchange事件触发,调用setState ==>

页面刷新,将最新的location对象通过Provider提供出去 ==>

在Route组件中通过Consumer获取数据。

3. 根据正则匹配路由,渲染相应组件

3.1 安装path-to-regexp
npm install path-to-regexp

path-to-regexp的github地址

3.2 Route组件中路由的匹配

Route组件的核心功能:
路由的匹配(注意exact属性的处理),根据匹配结果返回对应的组件。

//App.js
import React from 'react'
import { HashRouter as Router, Route } from './react-router-dom'

function Home() {
  return (
    <div className="Home">
      Home
    </div>
  )
}

...
...

class App extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (
      <div className="App">
        <Router>
          {/* 给Route组件传递path属性和component */}
          <Route path="/home" component={Home} />
        </Router>
      </div>
    )
  }
}

export default App

在Route组件中,根据props中的path生成正则对象。用Provider提供的pathname去match。

如果match到了,则处理参数。在我们关注的三个对象中,location和history是Provider提供过来的,match是根据每次路径中不同的hash值,渲染不同的组件,临时生成的。

//react-router-dom/Route.js
import React from 'react'
import { pathToRegexp } from 'path-to-regexp'
import { Consumer } from './context'

function Route(props) {
  return (
    <Consumer>
      {
        //定义一个函数,来消费Provider提供的数据
        value => {
          //拿到Provider提供的pathname
          const { pathname } = value.location

          //拿到组件传递过来的数据
          //exact默认为false
          //component重新命名为首字母大写
          const { path, component: Component, exact = false } = props

          //根据path生成正则
          const paramNames = []
          //end是ture表示在正则结尾添加$,是false就不添加
          const reg = pathToRegexp(path, paramNames, { end: exact })

          //匹配结果
          const result = pathname.match(reg)
          if (result) {
            //处理参数
            const names = paramNames.map(item => item.name)
            const [url, ...values] = result
            const params = {}
            names.forEach((name, index) => {
              params[name] = values[index]
            })
            const props = {
              location: value.location,
              history: value.history,
              match: {
                params,
                path,
                url,
                isExact: exact
              }
            }
            //将传递来的组件返回出去,进行渲染
            return <Component {...props} />
          }
          return null
        }
      }
    </Consumer>
  )
}

export default Route

4. Link组件的实现

4.1 Link组件

Link组件的本质是返回一个a标签,点击后跳转到指定的地址。这里我们就需要用到history上的push方法。

修改HashRouter.js,在history上添加push方法。

//HashRouter.js
import React, { Component } from 'react'
import { Provider } from './context'

class HashRouter extends Component {
  constructor(props){
    ...
  }
  componentDidMount() {
   ...
  }
  render() {
    const value = {
      location: this.state.location,
      history: {
        push(to){
          window.location.hash = to
        }
      }
    }
    //Provider提供数据  value就是传递出去的数据
    return (<Provider value={value}>{this.props.children}</Provider>)
  }
}

export default HashRouter

react-router-dom文件夹下新建Link.js。

注意: 在return 的a标签中,href属性如果写’javascript:;'会报一个警告,但是如果写"#",则会改变hash值。所以拼接字符串,写成每次传递来的hash值。

//react-router-dom/Link.js
import React from 'react'
import { Consumer } from './context'

function Link(props) {
  return (
    <Consumer>
      {
        //定义一个函数,来消费Provider提供的数据
        value => {
          //点击调用value.history.push方法
          return <a href={`#${props.to}`} onClick={()=>value.history.push(props.to)} >{props.children}</a>
        }
      }
    </Consumer>
  )
}

export default Link

在react-router-dom的index.js中引入并导出

//react-router-dom/index.js
//引入相关的组件
import HashRouter from './HashRouter'
import Route from './Route'
import Link from './Link'

//以对象的形式导出
export {
  HashRouter,
  Route,
  Link
}

5. Switch组件的实现

5.1 Switch组件

Switch组件的功能是,匹配到一个路由后,就不再往下匹配了。

我们在Switch组件中遍历所有的Route组件,比对路径,匹配到以后再return。

//react-router-dom/Switch.js
import React from 'react'
import { pathToRegexp } from 'path-to-regexp'
import { Consumer } from './context'

function Switch(props) {
  return (
    <Consumer>
      {
        //定义一个函数,来消费Provider提供的数据
        value => {
          const children = props.children
          //获取当前的pathname
          const pathname = value.location.pathname

          for (let i = 0, len = children.length; i < len; i++) {
            let child = children[i]
            //获取child的属性
            const { path="", exact=false } = child.props

            //生成正则
            const paramNames = []
            const reg = pathToRegexp(path, paramNames, { end: exact })

            //匹配结果
            const result = pathname.match(reg)
            if (result) {
              //如果匹配到,将当前child return出去
              return child
            }
          }
          //如果都每匹配到,返回null
          return null
        }
      }
    </Consumer>
  )
}

export default Switch
//react-router-dom/index.js
//引入相关的组件
import HashRouter from './HashRouter'
import Route from './Route'
import Link from './Link'
import Switch from './Switch'

//以对象的形式导出
export {
  HashRouter,
  Route,
  Link,
  Switch
}

6. Redirect组件的实现

6.1 Redirect组件

Redirect组件的核心改变路由让页面重写渲染。

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

function Redirect(props) {
  return (
    <Consumer>
      {
        //定义一个函数,来消费Provider提供的数据
        value => {
          value.history.push(props.to)
          return null
        }
      }
    </Consumer>
  )
}

export default Redirect
//react-router-dom/index.js
//引入相关的组件
import HashRouter from './HashRouter'
import Route from './Route'
import Link from './Link'
import Switch from './Switch'
import Redirect from './Redirect'

//以对象的形式导出
export {
  HashRouter,
  Route,
  Link,
  Switch,
  Redirect
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值