实现一个简易版的react-router

497 阅读3分钟

一、 新建脚手架并配置好文件夹

create-react-app my-router

image.png

二、建立context.js

import React from "react";
// 目的是给底下的子组件透传路由的信息
export default React.createContext()

三、封装BrowserRouter

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

export default class BrowserRouter extends Component{
  constructor(props) {
    super(props)
    // 定义路由状态对象
    this.state = {
      location: {
        // 获取当前路由路径
        pathname: window.location.pathname || "/",
        // 定义路由参数
        query: undefined
      },
      match: {
        
      }
    }
    
  }

  componentWillMount() {
     // 监测路由的前进后退
    window.addEventListener('popstate', () => {
      this.setState({
        // 更改最新location
        location: {
          pathname: window.location.pathname,
          match: this.state.match,
          history: {
            push: (to) => {
              // 根据当前to去匹配不同的路由 实现路由切换
              if(typeof to === 'object') {
                let { pathname, query } = to
                this.setState({
                  location: {
                    query,
                    pathname
                  }
                })
                // 进行路由跳转
                window.history.pushState({}, {}, pathname)
              }else {
                // 如果是字符串
                this.setState({
                  location: {
                    pathname: to
                  }
                })
                // 进行路由跳转
                window.history.pushState({}, {}, to)
              }
            }
          }
        }
      })
    })
  } 

  render() {
    // 保存当前路由信息
    const currentRoute = {
      // 更改最新location
      location: this.state.location,
      match: this.state.match,
      history: {
        push: (to) => {
          // 根据当前to去匹配不同的路由 实现路由切换
          if(typeof to === 'object') {
            let { pathname, query } = to
            this.setState({
              location: {
                query,
                pathname
              }
            })
            window.history.pushState({}, {}, pathname)
          }else {
            // 如果是字符串
            this.setState({
              location: {
                pathname: to
              }
            })
            window.history.pushState({}, {}, to)
          }
        }
      }
    }
    return (
      // 通过Context把数据透传到子组件
      <Context.Provider value={currentRoute}>{this.props.children}</Context.Provider>
    )
  }
}

四、安装path-to-regexp包

安装他的目的是匹配route中props属性的path, 简化了写正则的步骤

npm i path-to-regexp

五、封装Route组件

import React, {Component} from "react";
import Context from './context'
import { pathToRegexp, match } from "path-to-regexp"
export default class Route extends Component{
   // 将context定义到类上
  static contextType = context
  render() {
    const currentRoutePath = this.context.location.pathname // 从上下文context中获取当前路由
    const { path, component: Component, exact=false } = this.props // 获取Route组件props的路由
    const paramsRegexp = match(path, {end: exact}) // 获取params的表达式
    const matchResult = paramsRegexp(currentRoutePath)
    console.log(`路由匹配结果`, matchResult)
    this.context.match.params = matchResult.params
    const props = {
      ...this.context
    }
    const pathRegexp = pathToRegexp(path, [], {end: exact}) // 生成路径匹配表达式
    // 如果当前路径与传入的props的路径匹配, 那么渲染对应的组件, 并传递给对应的路由数据
    if(pathRegexp.test(currentRoutePath)) {
      return (<Component {...props}></Component>)
    }
    // 没有匹配到什么都不渲染
    return null
  }
}

六、将封装的路由组件在router文件夹暴露出去

index.js

import BrowserRouter from "./BrowserRouter";
import Route from "./Route";

// 将定义的路由组件暴露出去
export {
  BrowserRouter,
  Route
}

七、在pages文件夹,定义三个要渲染的组件

image.png

// Page1
export default function Page1() {
  return <div>Page1</div>
}
// Page2
export default function Page2() {
  return <div>Page2</div>
}
// Page3
export default function Page3() {
  return <div>Page3</div>
}

八、在router.js中使用路由

import Page1 from "./pages/page1";
import Page2 from "./pages/page2";
import Page3 from "./pages/page3";
import React, { Component } from "react";
import { Route, BrowserRouter } from './router'

class Router extends Component {
  constructor(props) {
    super(props)
    this.state = {

    }
  }
  
  render() {
    return (
      <BrowserRouter>
        <Route exact path="/" component={Page1} />
        <Route exact path="/page2" component={Page1} />
        <Route exact path="/page3" component={Page1} />
      </BrowserRouter>
    )
  }
}

九、在App中引入router

import Router from "./router";

export default function App() {
  return <Router></Router>
}

我们可以看到根路径 /匹配的是Page1

image.png

/page2 匹配的是Page2 说明我们大功告成了🎇

image.png

下面我再来封装个Link组件 用于点击跳转

十、封装Link组件

在routerDom文件夹中新建Link.css Link.js

image.png

Link.css

.link {
  text-decoration: none;
  color: orange;
  cursor: pointer;
}

Link.js

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

export default class Link extends Component {
  static contextType = context
  render() {
    // 获取要跳转的信息, to可以是对象也可以是字符串
    let {to} = this.props
    // 点击通过context的hittory的push方法进行跳转
    return <a className="link" href="" onClick={() => this.context.history.push(to)}>{this.props.children}</a>
  }
}

十、在pages组件引入Link跳转组件

// Page1
import Link from "../routerDom/Link"
export default function Page1() {
  return <div>
    Page1
    <Link to="/page2">跳转到page2</Link>
  </div>
}

image.png

点击跳转则跳转到了page2

image.png

十一、总结

React-router是通过context全局上下文来把路由参数(location)传递给他的子组件的。在BrowserRouter中 通过history路由的popstate监听路由的前进后退, 把最新的路径(window.location.pathname)传给Route组件, Route进行路径的匹配 展示对应的组件, Link组件通过history的push方法,调用window.history.pushState进行路由的跳转