react笔记--手动实现一个react-router(简易版)

前言

从vue转入到react技术栈有两月了,两个月来一直断断续续学习react的知识。自己也很久没有写过总结了(恐怖的加班),趁元旦假期抽空总结一波(还是要学习地)。习惯了vue简洁的语法和api,再回过来写react组件化,不习惯有木有(怪自己太菜)。

文中若有错误点,欢迎各位大佬指正


react-router路由的模式选择

用过react-router的会比较熟悉react路由模式,一般有两种,分别是hashHistory和history, 使用hashHistory模式,url后面会带有#号不太美观,而使用history模式,就是正常的url,但是如果匹配不到这个路由就会出现404请求。这种情况需要在服务器配置,如果URL匹配不到任何静态资源,就跳转到默认的index.html

两种方式实现原理
1.hashHistory路由

hash值变化不会导致浏览器向服务器发出请求,而且 hash 改变会触发 hashchange 事件,浏览器的进后退也能对其进行控制

如http://localhost:3000/detail#/home,这段url的#号后面的就为hash值
window.location.hash 取到的就是#home

 //监听hash变化
 window.addEventListener ('hashchange',  (e)=> {
        this.setState({
            ...this.state,
            location:{
              ...location,
               hash:window.location.hash
               pathname:window.location.hash
            },
      })
 });
复制代码

2.history路由

window.history 对象表示窗口的浏览历史,它只有back()、forward() 和 go() 方法可以让用户调用, 而h5规范中又新增了几个关于操作history记录的APi,分别是replaceState,pushState,popstate


在点击浏览器前进和后退的时候,都会触发popstate事件,而采用pushState和replaceState不会触发此事件,

代码示例
/*
  state   要跳转到的URL对应的状态信息,可以存一些需要想保存的值,也可以直接传{}
  title   该条记录的title,现在大多数浏览器不支持或者忽略这个参数
  url     这个参数提供了新历史纪录的地址,可以是相对路径,不可跨域
*/
window.history.pushState(state, title, url) 
//replaceState和pushState的不同之处在与,replace是替换栈顶上的那个元素,不会影响栈的长度
window.history.replaceState(state, title, url) 

//例子
 window.addEventListener('popstate',(e)=>{
      this.setState({
        ...this.state,
        location:{
          ...location,
          pathname:e.state.path,
        },
      })
 })
复制代码

实现路由

有了以上的知识点,就可以动手写组件了,在动手写组件之前,先来看看官方路由的具体用法,才能知道如何去设计这些组件

模块导入和导出
import {  HashRouter as Router, Route,Link, Redirect,Switch,} from 'react-router-dom';
复制代码

react-router-dom中引出了很多的组件,模块中向外部导出接口,常见的做法是文件夹中有一个index.js向外暴露出这个模块的所有接口,所以可以设计为react-router-dom文件夹会下有一堆组件,通过一个index.js,使用export defalut向外部导出接口对接


路由中的组件使用示例
//router.js  配置路由
export default const BasicRouter = () => {
    return (
        <div>
          <Router>
            <div>
            <div>
              <Link to="/home">首页</Link>
              <Link to="/detail">详情</Link>
            </div>
              <Switch>
                <Route  path="/home" component={Home} />
                <Route  path="/detail" component={Detail} />
                <Redirect to="/home" />
              </Switch>
            </div>
          </Router>
        </div>
    )
}
复制代码

可以看到Router是最外层的父组件,它里面的每个子组件都可以从props中拿到Router组件中的state,router看作是父组件,而里面的route、Switch组件等,一般做法是采用porps向下级传递的方法,但如父子组件中间跨了多个子组件,采用props传值就很麻烦,这里采用组件的context来传递共享数据

//使用路由后,在所有子组件中打印this.props,会发现有这一陀东西,这里只是router组件中的部分state状态
{
    history:{
      replace:e=>{},  
      push:e=>{},  
    },
    match:{
        params:'',
        isExact:false
    },
    location:{
        pathname:'',
        hash:'',
    }
}
复制代码

熟悉redux的人应该都知道,store中的共享状态需要通过一个顶层组件作为父组件,一般将顶级组件叫做Provider组件,由它内部创建context来作为数据的提供者
例如redux中的connect方法,它就是一个高阶组件,connext方法的参数在函数中通过解构拿到store中的数据,再通过props的方式给到connext传入的组件中,而在react 16.3版本中新增createContext方法,它返回了Provider, Consumer组件等,

context实现
//context.js

import React from 'react';
let { Provider,Consumer } = React.createContext()
export  { Provider, Consumer}

//顶级组件

import { Provider } from './context'
<Provider value={this.state}>
    {this.props.children}  
</Provider>

//所有的子级组件 Consumer里面的childer是一个函数,由函数来返回渲染的块,state就是provider传入的value

import { Consumer} from './context'
render(){
      <Consumer>
            {state => {
              //这里的state就是provider传入的value
              if(state.pathname===path){
               return  this.props.component
              }
              return null
            }}
       </Consumer>
}
复制代码

Provider组件实现了,其他的就比较好办了,在hashRouter顶级组件中使用Provider组件,里面每个子组件中外层采用Consumer包裹,这样每个组件都能拿到provider的数据

hashRouter.js实现

hashRouter用于提供hisotry的数据以及方法给到子组件,如push,go等方法

//react-router-dom文件夹下hashRouter.js

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

export default class HashRouter extends Component {
  constructor () {
    super (...arguments);
    this.state = {
      location: {
        pathname: window.location.hash.slice(1), //去除#号
        hash: window.location.hash,
      },
      history:{
        push(to){
            window.location.hash = to
        }
      }
    };
  }

  componentDidMount () {
    let location  = this.state
    window.addEventListener ('hashchange',  (e)=> {
      this.setState ({
        location: {
          ...location,
          hash:window.location.hash,
          pathname: window.location.hash.slice (1) || '',  //去除#号
        },
      });
    });
  }
  render () {
    return (
      <Provider value={this.state}>
        {this.props.children}
      </Provider>
    );
  }
}
复制代码

hashRouter组件state中的的push方法,直接将 window.location.hash值改变,会触发haschange时间,而在componentDidMount钩子函数中,监听hashchange事件中,在变化后将hash值存入state中

在componentWillUnmount记得要把绑定的事件解绑,remove事件需要将函数抽出来作为一个变量引用才能清除掉


Route.js实现

该组件用来传入component和path

import React, {Component} from 'react';
import { Consumer} from './context'
const pathToRegexp = require('path-to-regexp');

export default class Route extends Component {
  constructor () {
    super (...arguments)
  }
  render () {
    let { path, component: Component, exact=false } = this.props;
    return (
      <Consumer>
        {state => { 
        //pathToRegexp 方法,第一个参数,
          let reg= pathToRegexp(path,[],{end:exact })
          let pathname = state.location.pathname
          if (reg.test(pathname)) {
            return <Component {...state} />;
          }
          return null;
        }}
      </Consumer>
    );
  }
}
复制代码


正常情况下,url可能会有这几种情况,如/foo/bar, 或者/foo:123,这种url如果不处理,默认是匹配不到的,而exact参数就是控制是否精确匹配,这里引入了 pathToRegexp库来生成正则表达式,来处理 url 中地址查询参数

//示例代码


//如果需要精确匹配,将pathToRegexp的第三个参数end传为true,pathToRegexp第二个参数是匹配到的值
let ret = []
var re = pathToRegexp('/detail',ret,{
    end:true 
})
re.test('/foo/1')  // true

//生成的正则
/^\/detail(?:\/)?$/i                
/^\/detail(?:\/(?=$))?(?=\/|$)/i     

复制代码
Switch.js实现

用于匹配只渲染一个route组件

import React, {Component} from 'react';
import { Consumer} from './context'
const pathToRegexp = require('path-to-regexp');

export default class Switch extends Component {
  constructor () {
    super (...arguments);
  }

  render () {
    return (
      <Consumer>
        {state => {
          let pathname =state.location.pathname;
          let children = this.props.children
          for(let i=0;i<children.length;i++){
            let child = children[i]
            let path = child.props.path || ''
            let reg =  pathToRegexp(path,[],{end:false})
            if(reg.test(pathname)){
              return child
            }
          }
          return null
        }}
      </Consumer>
    );
  }
}


 //使用Switchs
 
 <Switch>
        <Route  path="/home" component={Home} />
        <Route  path="/detail" component={Detail} />
        <Redirect to="/home"/>
</Switch>

复制代码

Switch组件将传入的children,遍历拿到每一个组件传入的path,并生成正则,如果正则能够匹配的上,则直接渲染child,否则return null,确保switch中包裹的子组件,只能渲染其中一个,switch组件是用于配合redirect组件来使用的

redirect.js实现

用于重定向

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

export default class Redirect extends Component {
  constructor () {
    super (...arguments);
  }

  render () {
    return (
      <Consumer>
        {state => {
          let { history }= state;
          history.push(this.props.to)
          return null
        }}
      </Consumer>
    );
  }
}
复制代码

redirect组件实现非常简单,如果该组件渲染,直接将window.location.hash = to

browserRouter.js的实现

browserRouter与hashRouter的实现不同点是,在state的push方法中调用window.history.pushState,压入后,浏览器的url会直接变化页面不会刷新,另外popstate监听事件,也需要同步一次state里面的pathname

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

class browserRouter extends Component {
  constructor () {
    super (...arguments);
    this.state = {
      location: {
        pathname: window.location.pathname ,
        hash: window.location.hash,
      },
      history:{
        push :(to)=>{
          this.pushState(null,null,to)  
        }
      },
      queue:[]      
    };
    this.pushState = this.pushState.bind(this)
  }


  pushState = (state="",title="",path="")=>{
       let queue  = this.state.queue
       let {location}  = this.state 
       let historyInfo ={state,title,path}
       queue.push( historyInfo)
       this.setState({
        ...this.state,
        location:{
          ...location,
          pathname:path,
        },
        queue,
      })
      window.history.pushState(historyInfo,title,path)
  }

  componentDidMount () {
    let {location}  = this.state 
    window.addEventListener('popstate',(e)=>{
      this.setState({
        ...this.state,
        location:{
          ...location,
          pathname:e.state.path,
        },
        queue:this.state.queue,
      })
    })
  }

  render () {
    return (
      <Provider value={this.state}>
        {this.props.children}
      </Provider>
    );
  }
}

export default browserRouter;
复制代码
如何使用?

1.新建一个router.js,用于管理route组件
2.在index.js中导入使用


import React from 'react';
import {
   HashRouter as Router,
  //  BrowserRouter as Router,
  Route,
  Link,
  Redirect,
  Switch,
} from './react-router-dom';

import Home from './pages/home';
import Detail from './pages/detail';  

const BasicRoute = () => {
  return (
    <div>
      <Router>
        <div>
        <div>
          <Link to="/home">首页</Link>
          <Link to="/detail">详情</Link>
        </div>
          <Switch>
            <Route  path="/home" component={Home} />
            <Route  path="/detail" component={Detail} />
            <Redirect to="/home" />
          </Switch>
        </div>

      </Router>
    </div>
  );
};
export default BasicRoute;


// index.js中 使用

import Router from './router'
ReactDOM.render(<Router/>, document.getElementById('root'));

复制代码

结尾

简易版的router组件到这里就实现的差不多了,但是还是有很多功能没实现,比如query参数处理,link组件等,有兴趣可自行研究

代码地址 : github.com/huqc2513/re…

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值