實現react-router v4(上)

慕晨同學發表於2019-01-13

寫在前面

用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 v4(上)
如上圖所示,在上面的那個例子路由引入'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 應用設計模式 ——誠身

react-router@4.0 使用和原始碼解析 ——夏爾先生

相關文章