实现react-router v4(上)

写在前面

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

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

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

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

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

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组件。在这个例子当中。看下这行代码

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可以跨组件传递数据:

// 这是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 ---诚身的回答

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

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

实现HashRouter

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

// 这是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 应用设计模式 ——诚身

[email protected] 使用和源码解析 ——夏尔先生

猜你喜欢

转载自juejin.im/post/5c3abebb51882525a67c55c2