動手實現一個react路由

react路由經過不一樣的路徑渲染出不一樣的組件,這篇文章模擬 react-router-dom的api,從零開始實現一個react的路由庫

兩種實現方式

路由的實現方式有兩種:hash路由Browser路由react

HashRouter

HashRouter即經過hash實現頁面路由的變化,hash的應用很普遍,我最開始寫代碼時接觸到hash,通常用來作頁面導航和輪播圖定位的,hash值的變化咱們能夠經過hashchange來監聽:git

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事件:github

((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,因此我把主要的文件和導出內容也先寫好:npm

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存放裏面的數據:api

// context.js
import React from "react";
export default React.createContext();

HashRouter

接下來編寫HashRouter,做爲路由最外層的父組件,Router應該包含了提供給子組件所需的api:數組

//HashRouter.js

import 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信息:react-router

//HashRouter.js

//render
const $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,:app

//BrowserRouter.js

import 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參數進行正則匹配,若是匹配成功,就返回組件對應的Componentdom

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.js
import 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.js
import 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方法去渲染子組件

//MenuLink
import 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.js
import 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.js
import 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.js
import 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.js

import 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的路由插件就大功告成了!

相關文章
相關標籤/搜索