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.jsimport React from "react";export default React.createContext();

HashRouter

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

//HashRouter.jsimport 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//renderconst $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.jsimport 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.jsimport 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.jsimport 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办法去渲染子组件

//MenuLinkimport 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.jsimport 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.jsimport 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.jsimport 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.jsimport 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的路由插件就功败垂成了!