实现react-router v4(上)

文章首发于:github.com/USTB-musion…html

写在前面

用react-router v4能够实现单页面应用,能够将组件映射到路由上,将对应的组件渲染到想要渲染的位置。 react路由有两种方式:一种是HashRouter,即利用hash实现路由切换。另外一种是BrowserRouter,即利用html5 API实现路由的切换。本文是在阅读react-router v4源码以后简单的实现。html5

本文将从如下几部分进行总结:react

  1. 使用react-router v4的一个简单例子
  2. 代码结构
  3. Provider和Consumer
  4. 实现HashRouter
  5. 实现Route
  6. 实现Link
  7. 实现Redirect
  8. 实现Switch

使用react-router的一个简单例子

如下是参照react-router 官方文档实现的一个简单例子:git

import React, { Component } from 'react';
import { render } from 'react-dom';
import { HashRouter as Router, Route, Link, Redirect, Switch } from 'react-router-dom';
import Home from './Home';
import Profile from './Profile';
import User from './User';

export default class App extends Component {
  constructor() {
    super();
  }
  
  render() {
    return (
      <Router>
        <div>
          <div>
            <Link to="/home">首页</Link>
            <Link to="/profile">我的中心</Link>
            <Link to="/user">用户</Link>
          </div>
          <div>
            <Switch>
              <Route path="/home" exact={true} component={Home}></Route>
              <Route path="/profile" component={Profile}></Route>
              <Route path="/user" component={User}></Route>
              <Redirect to="/home"></Redirect>
            </Switch>
          </div>
        </div>
      </Router>
    )
  }
}

render(<App></App>, document.querySelector('#root'));
复制代码

这样就能实现一个超级简单的单页面应用,根据路径的变化渲染相应的组件。点击首页会跳转到Home组件,点击我的中心会跳转到Profile组件,点击用户会跳转到User组件。在这个例子当中。看下这行代码github

import { HashRouter as Router, Route, Link, Redirect, Switch } from 'react-router-dom';
复制代码

若是不引入'react-router-dom'这个包,而是本身实现一个my-react-router-dom,暴露出HashRouter,Route,Link,Redirect,Switch这几个组件,而且这几个组件和react-router-dom提供的功能基本同样,那究竟怎么实现呢?设计模式

代码结构

如上图所示,在上面的那个例子路由引入'react-router-dom'改成'./my-react-router-dom', 并在同级新建一个my-react-router-dom文件夹,并在index.js中暴露出 HashRouter, Route, Link, Redirect, Switch这几个方法。

// 这是index.js文件
import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
import Redirect from './Redirect';
import Switch from './Switch';

export {
  HashRouter,
  Route,
  Link,
  Redirect,
  Switch
}
复制代码

Provider和Consumer

再看一下context.js文件,context能够跨组件传递数据:api

// 这是context.js文件
import React, { Component } from 'react';
// 这个方法是16.3新增的
let { Provider, Consumer} = React.createContext();

export { Provider, Consumer};
复制代码

旧版context的致命缺陷

‘现有的原生 Context API 存在着一个致命的问题,那就是在 Context 值更新后,顶层组件向目标组件 props 透传的过程当中,若是中间某个组件的 shouldComponentUpdate 函数返回了 false,由于没法再继续触发底层组件的 rerender,新的 Context 值将没法到达目标组件。这样的不肯定性对于目标组件来讲是彻底不可控的,也就是说目标组件没法保证本身每一次均可以接收到更新后的 Context 值。’如何解读 react 16.3 引入的新 context api ---诚身的回答bash

新版 Context API 提供Provider和Consumer两个组件,顾名思义,provider(提供者)和Consumer(消费者):react-router

  • Provider 组件:用在组件树中更外层的位置。它接受一个名为 value 的 prop,其值能够是任何 JavaScript 中的数据类型。
  • Consumer 组件:能够在 Provider 组件内部的任何一层使用。它接收一个名为 children 值为一个函数的 prop。这个函数的参数是 Provider 组件接收的那个 value prop 的值,返回值是一个 React 元素(一段 JSX 代码)。

实现HashRouter

分析上面的例子,HashRouter的别名设置为Router,传入的 Router 中的每一项即为一条路由配置,表示在匹配给定的地址时,应该使用什么组件渲染视图。HashRouter的简单实现以下:dom

// 这是hashRouter.js文件
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from './context';

export default class HashRouter extends Component {
  constructor() {
    super();
    this.state = {
      location: {
        // slice(1)将#截掉
        pathname: window.location.hash.slice(1) || ''
      }
    };
  }

  componentDidMount() {
    // 首次进入页面url会显示#/
    // 如localhost:3000会显示为localhost:3000/#/
    window.location.hash = window.location.hash || '/';
    // 监听hash值变化,从新设置location状态
    window.addEventListener('hashchange', () => {
      this.setState({
        location: {
          ...this.state.location,
          pathname: window.location.hash.slice(1) || '/'
        }
      })
    })
  }

  render() {
    // 每一个子route对象都会包含location
    let value = {
      location: this.state.location,
      history: {
        push(to) {
          window.location.hash = to;
        }
      }
    }
    return (
      <Provider value={value}>
        {this.props.children}
      </Provider>
    )
  }
}

复制代码

这样一来,使用hashRouter的方式第一次进入页面时,将显示#/,如localhost:3000首次进入页面将会显示localhost:3000/#/,经过Provider组件来将location和history对象传递给子route,经过hashchange方法来监听hash值的变化,进而从新设置location的状态。

实现Route

经过分析上面的例子,看这一行代码:

<Route path="/home" exact={true} component={Home}></Route>
复制代码

发现Route组件包含有path,exact,component这些属性,那如何实现Route组件呢?看Route.js文件:

// 这是route.js文件
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';
import pathToReg from 'path-to-regexp';

export default class Router extends Component {
  constructor() {
    super();
  }

  render() {
    return (
      <Consumer>
        {state => {
          // <route path="xx" component="xx" exact={true}></route>
          // path是route传递的
          let { path, component: Component, exact = false } = this.props;
          // pathname是location中的
          let pathname = state.location.pathname;
          // 根据path实现一个正则,经过正则匹配
          // location中的/home/123是能匹配到Home组件的
          let keys = [];
          let reg = pathToReg(path, keys, { end: exact});
          keys = keys.map(item => item.name);
          let result = pathname.match(reg);
          let [url, ...values] = result || [];
          // 实现路由跳转
          let props = {
            location: state.location,
            history: state.history,
            match: {
              params: keys.reduce((obj, current, index) => {
                obj[current] = values[index];
                return obj;
              }, {})
            }
          }
          if (result) {
            return <Component {...props}></Component>
          }
          return null
        }}
      </Consumer>
    )
  }
}
复制代码

利用path-to-regexp这个库来进行是否严格匹配路径,利用新版context的Consumer组件和props来取出path,component,axact这几个参数。若是匹配到path,则经过<Component {...props}>返回相应匹配到的组件。

实现Link

分析上面的例子,Link的组件实现稍微简单一些,点击内容跳转到to属性对应的路径便可:

// 这是Link组件
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';

export default class Link extends Component {
  constructor() {
    super();
  }

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

经过history.push便可实现点击this.props.children的内容跳转到Link组件的to属性对应的路径。

实现Redirect

重定向就是匹配不到后直接跳转到Redirect中的to路径:

// 这是Redirect.js文件
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';

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

  render() {
    return (
      <Consumer>
        {state => {
          // 重定向就是匹配不到后直接跳转到redirect中的to路径
          state.history.push(this.props.to);
          return null;
        }}
      </Consumer>
    )
  }
}
复制代码

实现Switch

Switch组件的做用就是只匹配第一个匹配到的组件:

// 这是Switch.js的文件
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';
import pathToRegExp from 'path-to-regexp';

// Switch的做用就是匹配一个组件
export default class Switch extends Component {
  constructor() {
    super();
  }

  render() {
    return (
      <Consumer>
        {state => {
          {
            let pathname = state.location.pathname;
            // 取出Switch包含的组件
            let children = this.props.children;
            for ( var i = 0; i < children.length; i++) {
              let child = children[i];
              // Redirect组件可能没有path属性
              let path = child.props.path || '';
              pathToRegExp(path, [], {end: false});
              // switch匹配成功了
              if (reg.test(pathname)) {
                // 将匹配到的组件返回便可
                return child;
              }
            }
            return null;
          }
        }}
      </Consumer>
    )
  }
}
复制代码

经过遍历this.props.children来进行逐一匹配,若是匹配到相应的路径,当即返回对应的组件,若是匹配不到,则返回空。

总结

经过上面的例子,发现实现一个简易版的react-router并非想象中那么难。因为时间关系,路由权限校验,BrowserRouter,withRoute这些组件并无实现。下次有时间再好好分析思考一下如何实现:)

扩展阅读

React 全新的 Context API —— qiqi105

重新的 Context API 看 React 应用设计模式 ——诚身

react-router@4.0 使用和源码解析 ——夏尔先生

相关文章
相关标签/搜索