react筆記--手動實現一個react-router(簡易版)

漸行漸遠君發表於2019-01-01

前言

從vue轉入到react技術棧有兩月了,兩個月來一直斷斷續續學習react的知識。自己也很久沒有寫過總結了(恐怖的加班),趁元旦假期抽空總結一波(還是要學習地)。習慣了vue簡潔的語法和api,再回過來寫react元件化,不習慣有木有(怪自己太菜)。

文中若有錯誤點,歡迎各位大佬指正


react-router路由的模式選擇

用過react-router的會比較熟悉react路由模式,一般有兩種,分別是hashHistory和history, 使用hashHistory模式,url後面會帶有#號不太美觀,而使用history模式,就是正常的url,但是如果匹配不到這個路由就會出現404請求。這種情況需要在伺服器配置,如果URL匹配不到任何靜態資源,就跳轉到預設的index.html

兩種方式實現原理

1.hashHistory路由

hash值變化不會導致瀏覽器向伺服器發出請求,而且 hash 改變會觸發 hashchange 事件,瀏覽器的進後退也能對其進行控制

如http://localhost:3000/detail#/home,這段url的#號後面的就為hash值
window.location.hash 取到的就是#home

 //監聽hash變化
 window.addEventListener ('hashchange',  (e)=> {
        this.setState({
            ...this.state,
            location:{
              ...location,
               hash:window.location.hash
               pathname:window.location.hash
            },
      })
 });
複製程式碼

2.history路由

window.history 物件表示視窗的瀏覽歷史,它只有back()、forward() 和 go() 方法可以讓使用者呼叫, 而h5規範中又新增了幾個關於操作history記錄的APi,分別是replaceState,pushState,popstate


在點選瀏覽器前進和後退的時候,都會觸發popstate事件,而採用pushState和replaceState不會觸發此事件,

程式碼示例
/*
  state   要跳轉到的URL對應的狀態資訊,可以存一些需要想儲存的值,也可以直接傳{}
  title   該條記錄的title,現在大多數瀏覽器不支援或者忽略這個引數
  url     這個引數提供了新歷史紀錄的地址,可以是相對路徑,不可跨域
*/
window.history.pushState(state, title, url) 
//replaceState和pushState的不同之處在與,replace是替換棧頂上的那個元素,不會影響棧的長度
window.history.replaceState(state, title, url) 

//例子
 window.addEventListener('popstate',(e)=>{
      this.setState({
        ...this.state,
        location:{
          ...location,
          pathname:e.state.path,
        },
      })
 })
複製程式碼

實現路由

有了以上的知識點,就可以動手寫元件了,在動手寫元件之前,先來看看官方路由的具體用法,才能知道如何去設計這些元件

模組匯入和匯出
import {  HashRouter as Router, Route,Link, Redirect,Switch,} from 'react-router-dom';
複製程式碼

react-router-dom中引出了很多的元件,模組中向外部匯出介面,常見的做法是資料夾中有一個index.js向外暴露出這個模組的所有介面,所以可以設計為react-router-dom資料夾會下有一堆元件,通過一個index.js,使用export defalut向外部匯出介面對接


路由中的元件使用示例
//router.js  配置路由
export default const BasicRouter = () => {
    return (
        <div>
          <Router>
            <div>
            <div>
              <Link to="/home">首頁</Link>
              <Link to="/detail">詳情</Link>
            </div>
              <Switch>
                <Route  path="/home" component={Home} />
                <Route  path="/detail" component={Detail} />
                <Redirect to="/home" />
              </Switch>
            </div>
          </Router>
        </div>
    )
}
複製程式碼

可以看到Router是最外層的父元件,它裡面的每個子元件都可以從props中拿到Router元件中的state,router看作是父元件,而裡面的route、Switch元件等,一般做法是採用porps向下級傳遞的方法,但如父子元件中間跨了多個子元件,採用props傳值就很麻煩,這裡採用元件的context來傳遞共享資料

//使用路由後,在所有子元件中列印this.props,會發現有這一陀東西,這裡只是router元件中的部分state狀態
{
    history:{
      replace:e=>{},  
      push:e=>{},  
    },
    match:{
        params:'',
        isExact:false
    },
    location:{
        pathname:'',
        hash:'',
    }
}
複製程式碼

熟悉redux的人應該都知道,store中的共享狀態需要通過一個頂層元件作為父元件,一般將頂級元件叫做Provider元件,由它內部建立context來作為資料的提供者
例如redux中的connect方法,它就是一個高階元件,connext方法的引數在函式中通過解構拿到store中的資料,再通過props的方式給到connext傳入的元件中,而在react 16.3版本中新增createContext方法,它返回了Provider, Consumer元件等,

context實現
//context.js

import React from 'react';
let { Provider,Consumer } = React.createContext()
export  { Provider, Consumer}

//頂級元件

import { Provider } from './context'
<Provider value={this.state}>
    {this.props.children}  
</Provider>

//所有的子級元件 Consumer裡面的childer是一個函式,由函式來返回渲染的塊,state就是provider傳入的value

import { Consumer} from './context'
render(){
      <Consumer>
            {state => {
              //這裡的state就是provider傳入的value
              if(state.pathname===path){
               return  this.props.component
              }
              return null
            }}
       </Consumer>
}
複製程式碼

Provider元件實現了,其他的就比較好辦了,在hashRouter頂級元件中使用Provider元件,裡面每個子元件中外層採用Consumer包裹,這樣每個元件都能拿到provider的資料

hashRouter.js實現

hashRouter用於提供hisotry的資料以及方法給到子元件,如push,go等方法

//react-router-dom資料夾下hashRouter.js

import React, {Component} from 'react';
import {Provider} from './context';

export default class HashRouter extends Component {
  constructor () {
    super (...arguments);
    this.state = {
      location: {
        pathname: window.location.hash.slice(1), //去除#號
        hash: window.location.hash,
      },
      history:{
        push(to){
            window.location.hash = to
        }
      }
    };
  }

  componentDidMount () {
    let location  = this.state
    window.addEventListener ('hashchange',  (e)=> {
      this.setState ({
        location: {
          ...location,
          hash:window.location.hash,
          pathname: window.location.hash.slice (1) || '',  //去除#號
        },
      });
    });
  }
  render () {
    return (
      <Provider value={this.state}>
        {this.props.children}
      </Provider>
    );
  }
}
複製程式碼

hashRouter元件state中的的push方法,直接將 window.location.hash值改變,會觸發haschange時間,而在componentDidMount鉤子函式中,監聽hashchange事件中,在變化後將hash值存入state中

在componentWillUnmount記得要把繫結的事件解綁,remove事件需要將函式抽出來作為一個變數引用才能清除掉


Route.js實現

該元件用來傳入component和path

import React, {Component} from 'react';
import { Consumer} from './context'
const pathToRegexp = require('path-to-regexp');

export default class Route extends Component {
  constructor () {
    super (...arguments)
  }
  render () {
    let { path, component: Component, exact=false } = this.props;
    return (
      <Consumer>
        {state => { 
        //pathToRegexp 方法,第一個引數,
          let reg= pathToRegexp(path,[],{end:exact })
          let pathname = state.location.pathname
          if (reg.test(pathname)) {
            return <Component {...state} />;
          }
          return null;
        }}
      </Consumer>
    );
  }
}
複製程式碼


正常情況下,url可能會有這幾種情況,如/foo/bar, 或者/foo:123,這種url如果不處理,預設是匹配不到的,而exact引數就是控制是否精確匹配,這裡引入了 pathToRegexp庫來生成正規表示式,來處理 url 中地址查詢引數

//示例程式碼


//如果需要精確匹配,將pathToRegexp的第三個引數end傳為true,pathToRegexp第二個引數是匹配到的值
let ret = []
var re = pathToRegexp('/detail',ret,{
    end:true 
})
re.test('/foo/1')  // true

//生成的正則
/^\/detail(?:\/)?$/i                
/^\/detail(?:\/(?=$))?(?=\/|$)/i     

複製程式碼
Switch.js實現

用於匹配只渲染一個route元件

import React, {Component} from 'react';
import { Consumer} from './context'
const pathToRegexp = require('path-to-regexp');

export default class Switch extends Component {
  constructor () {
    super (...arguments);
  }

  render () {
    return (
      <Consumer>
        {state => {
          let pathname =state.location.pathname;
          let children = this.props.children
          for(let i=0;i<children.length;i++){
            let child = children[i]
            let path = child.props.path || ''
            let reg =  pathToRegexp(path,[],{end:false})
            if(reg.test(pathname)){
              return child
            }
          }
          return null
        }}
      </Consumer>
    );
  }
}


 //使用Switchs
 
 <Switch>
        <Route  path="/home" component={Home} />
        <Route  path="/detail" component={Detail} />
        <Redirect to="/home"/>
</Switch>

複製程式碼

Switch元件將傳入的children,遍歷拿到每一個元件傳入的path,並生成正則,如果正則能夠匹配的上,則直接渲染child,否則return null,確保switch中包裹的子元件,只能渲染其中一個,switch元件是用於配合redirect元件來使用的

redirect.js實現

用於重定向

import React, {Component} from 'react';
import { Consumer} from './context'

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

  render () {
    return (
      <Consumer>
        {state => {
          let { history }= state;
          history.push(this.props.to)
          return null
        }}
      </Consumer>
    );
  }
}
複製程式碼

redirect元件實現非常簡單,如果該元件渲染,直接將window.location.hash = to

browserRouter.js的實現

browserRouter與hashRouter的實現不同點是,在state的push方法中呼叫window.history.pushState,壓入後,瀏覽器的url會直接變化頁面不會重新整理,另外popstate監聽事件,也需要同步一次state裡面的pathname

import React, {Component} from 'react';
import {Provider} from './context';

class browserRouter extends Component {
  constructor () {
    super (...arguments);
    this.state = {
      location: {
        pathname: window.location.pathname ,
        hash: window.location.hash,
      },
      history:{
        push :(to)=>{
          this.pushState(null,null,to)  
        }
      },
      queue:[]      
    };
    this.pushState = this.pushState.bind(this)
  }


  pushState = (state="",title="",path="")=>{
       let queue  = this.state.queue
       let {location}  = this.state 
       let historyInfo ={state,title,path}
       queue.push( historyInfo)
       this.setState({
        ...this.state,
        location:{
          ...location,
          pathname:path,
        },
        queue,
      })
      window.history.pushState(historyInfo,title,path)
  }

  componentDidMount () {
    let {location}  = this.state 
    window.addEventListener('popstate',(e)=>{
      this.setState({
        ...this.state,
        location:{
          ...location,
          pathname:e.state.path,
        },
        queue:this.state.queue,
      })
    })
  }

  render () {
    return (
      <Provider value={this.state}>
        {this.props.children}
      </Provider>
    );
  }
}

export default browserRouter;
複製程式碼

如何使用?

1.新建一個router.js,用於管理route元件
2.在index.js中匯入使用


import React from 'react';
import {
   HashRouter as Router,
  //  BrowserRouter as Router,
  Route,
  Link,
  Redirect,
  Switch,
} from './react-router-dom';

import Home from './pages/home';
import Detail from './pages/detail';  

const BasicRoute = () => {
  return (
    <div>
      <Router>
        <div>
        <div>
          <Link to="/home">首頁</Link>
          <Link to="/detail">詳情</Link>
        </div>
          <Switch>
            <Route  path="/home" component={Home} />
            <Route  path="/detail" component={Detail} />
            <Redirect to="/home" />
          </Switch>
        </div>

      </Router>
    </div>
  );
};
export default BasicRoute;


// index.js中 使用

import Router from './router'
ReactDOM.render(<Router/>, document.getElementById('root'));

複製程式碼

結尾

簡易版的router元件到這裡就實現的差不多了,但是還是有很多功能沒實現,比如query引數處理,link元件等,有興趣可自行研究

程式碼地址 : github.com/huqc2513/re…

相關文章