乐趣区

关于react.js:动手实现一个react路由

react 路由通过不同的门路渲染出不同的组件,这篇文章模仿 react-router-dom 的 api,从零开始实现一个 react 的路由库

两种实现形式

路由的实现形式有两种:hash 路由 Browser 路由

HashRouter

HashRouter即通过 hash 实现页面路由的变动,hash的利用很宽泛,我最开始写代码时接触到 hash,个别用来做页面导航和轮播图定位的,hash 值的变动咱们能够通过 hashchange 来监听:

window.addEventListener('hashchange',()=>{console.log(window.location.hash);
});

BrowserRouter

浏览器路由的变动通过 h5 的 pushState 实现,pushState是全局对象 history 的办法,pushState会往 History 写入一个对象, 存储在 History 包含了 length 长度和 state 值,其中 state 能够退出咱们自定义的数据信息传递給新页面,pushState办法咱们能够通过浏览器提供的 onpopstate 办法监听,不过浏览器没有提供 onpushstate 办法,还须要咱们入手去实现它,当然如果只是想替换页面不增加到 history 的历史记录中,也能够应用 replaceState 办法,更多 history 能够查看 MDN,这里咱们给浏览器加上 onpushstate 事件:

((history)=>{
  let pushState = history.pushState; // 先把旧的 pushState 办法存储起来

  // 重写 pushState 办法
  history.pushState=function(state,title,pathname){if (typeof window.onpushstate === "function"){window.onpushstate(state,pathname);
    }
    return pushState.apply(history,arguments);
  }
})(window.history);

筹备入口文件

首先新建 react-router-dom 的入口文件index.js,这篇文章会实现外面次要的 api,所以我把次要的文件和导出内容也先写好:

import HashRouter from  "./HashRouter";
import BrowserRouter from  "./BrowserRouter";

import Route from  "./Route";
import Link from  "./Link";
import MenuLink from  "./MenuLink";

import Switch from  "./Switch";
import Redirect from  "./Redirect";

import Prompt from  "./Prompt";
import WithRouter from  "./WithRouter";

export {
  HashRouter,
  BrowserRouter,

  Route,
  Link,
  MenuLink,

  Switch,
  Redirect,

  Prompt,
  WithRouter
}

Context

蕴含在路由外面的组件,能够通过 props 拿到路由的 api 的,所以 react-router-dom 应该有一个属于本人的 Context,所以咱们新建一个context 寄存外面的数据:

// context.js
import React from "react";
export default React.createContext();

HashRouter

接下来编写 HashRouter,作为路由最外层的父组件,Router 应该蕴含了提供给子组件所需的 api:

//HashRouter.js

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

export default class HashRouter extends Component {render() {
    const value={history:{},
      location:{}}
    return (<RouterContext.Provider value={value}>
        {this.props.children}
      </RouterContext.Provider>
    )
  }
}

react路由最次要的是通过监听路由的变动,渲染出不同的组件,这里咱们能够先在 hashRouter 监听路由变动,再传递给子组件路由的变动信息,所有我么须要一个 state 来存储变动的 location 信息,并且可能监听到它的变动:

//HashRouter.js
...
export default class HashRouter extends Component {
  state = {
    location: {pathname:location.hash.slice(1)
    }
  }
  componentDidMount(){window.addEventListener("hashchange",(event)=>{
        this.setState({
          location:{
            ...this.state.location,
            pathname:location.hash.slice(1)
          }
        })
    })
  }
  render() {
    const value={history:{},
      location: this.state.location
    }
    ...
  }
}

同时给 history 增加上 push 办法,而且咱们晓得 history 是能够携带自定义 state 信息的,所有咱们也在组件外面定义 locationState 属性,存储路由的 state 信息:

//HashRouter.js

//render
const $comp = this;
const value = {
  history: {push(to){// to 可能是一个对象:{pathname,state}
      if (typeof to === "object") {
        location.hash = to.pathname;
        $comp.locationState = to.state;
      } else {
        location.hash = to;
        $comp.locationState = null;
      }
    }
  },
  location: {
      state: $comp.locationState, //locationState 存储路由 state 信息
      pathname: this.state.location.pathname
  }
}

BrowserRouter

BrowserRouterhashRouter 很像,只是监听的对象不一样了,监听的事件变成了 onpopstateonpushstate,并且页面跳转用 history.pushState,其它跟hashRouter 一样, 咱们新建BrowserRouter.js,:

//BrowserRouter.js

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

// 重写 pushState
((history) => {
  let pushState = history.pushState;
  history.pushState = function (state, title, pathname) {if (typeof window.onpushstate === "function") {
      // 增加 onpushstate 事件的监听
      window.onpushstate(state, pathname);
    }
    return pushState.apply(history, arguments);
  }
})(window.history);

export default class HashRouter extends Component {state = { location: { pathname: location.hash.slice(1) } };
  
  componentDidMount() {
      // 监听浏览器后退事件
    window.onpopstate = (event) => {
      this.setState({
        location: {
          ...this.state.location,
          state: event.state,
          pathname: event.pathname,
        }
      })
    }
    // 监听浏览器后退事件,自定义
    window.onpushstate = (state, pathname) => {
      this.setState({
        location: {
          ...this.state.location,
          state, pathname
        }
      })
    }
  }
  render() {
    const value = {
      history: {push(to) {if (typeof to === "object") {history.pushState(to.state, '', to.pathname);
          } else {history.pushState('','', to);
          }
        },
      },
      location: this.state.location
    }
    return (<RouterContext.Provider value={value}>
        {this.props.children}
      </RouterContext.Provider>
    )
  }
}

Route

Route组件靠 pathcomponent两个参数渲染页面组件,逻辑是拿以后 url 门路跟组件的 path 参数进行正则匹配,如果匹配胜利,就返回组件对应的Component

path-to-regexp

门路的正则转换用的是 path-to-regexp,门路还依据组件参数 exact 判断是否全匹配,转换后的正则能够通过 regulex*%3F%24)进行测试,首先装置path-to-regexp:

npm install path-to-regexp --save

path-to-regexp应用:

const keys = [];
const regexp = pathToRegexp("/foo/:bar", keys);
// regexp = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{name: 'bar', prefix: '/', suffix: '', pattern:'[^\\/#\\?]+?', modifier:'' }]

Route.js

新建Route.js

//Route.js
import React, {Component} from 'react'
import RouterContext from "./context"
import {pathToRegexp} from "path-to-regexp"

export default class Route extends Component {
  static contextType = RouterContext;
  render() {
    // 获取 url 门路 pathname 和 组件参数 path 进行正则匹配
    const {pathname} = this.context.location;
    const {path="/",component:Component,exact=false} = this.props;

    const keys = []; // 正则匹配的 keys 数组汇合
    const regexp = pathToRegexp(path,keys,{end:exact});
    const result = pathname.match(regexp); // 取得匹配后果

    // 将 context 的值传递给子组件应用
    let props = {
      history:this.context.history,
      location:this.context.location,
    }
    
    if (result){
      // 如果匹配胜利,获取门路参数
      const match = {};
      // 将 result 解构进去,第一个是 url 门路
      const [url,...values] = result; 
      // 将门路参数提取进去
      const params = keys.map(k=>k.name).reduce((total,key,i)=>(total[key]=values[i]),{});

      match = {url,path,params,isExact:url===pathname};
      props.match = match;

      return <Component {...props} />
    }
    
    return null;   

  }
}

render 和 children

Route组件还提供了两个参数 renderchildren,这两个参数可能让组件通过函数进行渲染,并将 props 作为参数提供,这在编写高阶函数中十分有用:

<Route path="/" component={Comp} render={(props) => <Comp {...props}/>}></Route>
<Route path="/" component={Comp} children={(props) => <Comp {...props}/>}></Route>

所以咱们在 Route 组件中也解构出 renderchildren,如果有这两个参数的状况下,间接执行返回后的后果并把 props 传进去:

//Route.js
...
const {render, children} = this.props;
...
if (result) {if (render) return render(props);
  if (children) return children(props);

  return <Component {...props} />
}

if (render) return render(props);
if (children) return children(props);

return null;

Link 和 MenuLink

LinkMenuLink 两个组件都提供了路由跳转的性能,其实是包装了一层的 a 标签,不便用户在 hashbrowser路由下都能放弃同样的跳转和传参操作,而 MenuLink 还给以后路由匹配的组件增加 active 类名,不便款式的管制。

Link

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

export default class Link extends Component {
  static contextType = RouterContext;
  render() {
    let to = this.props.to;
    return (<a {...this.props} onClick={()=>this.context.history.push(to)}>{this.props.children}</a>
    )
  }
}

MenuLink

MenuLink间接用函数组件,这里用到了 Routechildren办法去渲染子组件

//MenuLink
import React from 'react'
import {Link,Route} from "../react-router-dom"

export default function MenuLink({to,exact,children,...rest}) {let pathname = (typeof to === "object") ? to.pathname : to;
  return <Route path={pathname} 
                exact={exact} 
                children={(props)=>(<Link {...rest} className={props.match ? 'active' : ''} to={to}>{children}</Link>
                )} />
}

Switch 和 Redirect

Redirect组件重定向到指定的组件,不过须要配合 Switch 组件应用

Redirect

Redirect的逻辑很简略,就是拿到参数 to 间接执行跳转办法

//Redirect.js
import React, {Component} from 'react'
import RouterContext from "./context"

export default class Redirect extends Component {
  static contextType = RouterContext;
  componentDidMount(){this.context.history.push(this.props.to);
  }
  render() {return null;}
}

Switch

能够看到 Redirect 组件就是间接重定向到指定门路,如果在组件中间接引入到了这里就间接跳转了,所以咱们要写个 Switch 配合它:

//Switch.js
import React, {Component} from 'react'
import RouterContext from "./context";
import {pathToRegexp} from "path-to-regexp";

export default class Switch extends Component {
  static contextType = RouterContext;
  render() {
    // 取出 Switch 外面的子组件, 遍历查找出跟路由门路雷同的组件返回,如果没有,就会到 Redirect 组件中去
    let {children} = this.props; 
    let {pathname} = this.context.location;
    for (let i = 0, len = children.length;i<len;i++){let {path="/",exact=false} = children[i].props;
      let regexp = pathToRegexp(path,[],{end:exact});
      let result = pathname.match(regexp);

      if (result) return children[i];
    }
    return null
  }
}

WithRouter 和 Prompt

WithRouter

WithRouter是一个高阶函数,通过它的包装能够让组件享有路由的办法:

//WithRouter.js
import React, {Component} from 'react'
import {Route} from '../react-router-dom';

function WithRouter(WrapperComp) {return () => (<Route render={(routerProps) => <WrapperComp {...routerProps} />} />
  )
}

export default WithRouter

Prompt

Prompt用的比拟少,组件中如果须要用到它,须要提供 whenmessage两个参数,给用户提示信息:

//Prompt.js

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

export default class Prompt extends Component {
  static contextType = RouterContext;
  componentWillUnmount() {this.context.history.unBlock();
  }
  render() {let { message, when} = this.props;
    let {history, location} = this.context;
    if (when) {history.block(message(location))
    } else {history.unBlock();
    }
    return null;
  }
}

能够看到,history中新增了 blockunBlock办法,用来显示提醒的信息,所以要到 history 中增加这两个办法,并在路由跳转的时候截取,如果有信息须要提醒,就给予提醒:

//HashRouter.js && BrowserRouter.js
...
history: {push(){if ($comp.message) {let confirmResult = confirm($comp.message);
      if (!confirmResult) return;
      $comp.message = null;
    }
    ...
  },
  block(message){$comp.message = message},
  unBlock(){$comp.message = null;}
}
...

ok! 到了这里,一个 react 的路由插件就功败垂成了!

退出移动版