實現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 使用和源碼解析 ——夏爾先生

相關文章
相關標籤/搜索