讓react用起來更得心應手——(react-router原理簡析)

夢想攻城獅發表於2018-10-23

讓react用起來更得心應手系列文章:

  1. 讓react用起來更得心應手——(react基礎簡析)
  2. 讓react用起來更得心應手——(react-router原理簡析)
  3. 讓react用起來更得心應手——(react-redux原理簡析)

前端路由和後臺路由

在剛入行的時候一直明白什麼單頁面應用是什麼,說白了就是混淆了前臺路由和後臺路由,現在來縷縷它們:

  1. 前臺路由:頁面的顯示由前臺js控制,在url的路徑中輸入雜湊值是不會往後臺傳送請求的,所以前臺可以通過將雜湊和頁面進行對映從而控制渲染顯示哪個頁面。
  2. 後臺路由:頁面的顯示由後臺根據url進行處理然後返回給瀏覽器,非雜湊url都會往伺服器傳送請求(historyAPI也不會傳送請求,後面會介紹)

如果還不理解,那麼可以用express搭建本地伺服器看看效果(ps:為什麼用express,因為懶,koa的話還得下載koa-router外掛):

var express = require('express');
var app = express();

app.get('/', function (req, res) {
    res.send('welcome to home');
 }) 
app.get('/a', function (req, res) {
   res.send('welcome to a');
})
var server = app.listen(8081)
複製程式碼

在瀏覽器中輸入localhost:8081/

home
在瀏覽器中輸入localhost:8081/#a
#a
在瀏覽器中輸入localhost:8081/a
/a
結合圖片和上面的陳述應該知道前端路由和後臺路由的區別,除了hash路由還有一種方法可以修改url並且不向後臺傳送請求,它是history.pushState(),注意相容處理:
history-push
history-result
但是這種方法有一個問題,如果再按一次Enter鍵,它是會向後臺傳送請求的,如果後臺路由沒有相應的匹配,那麼會報404的錯誤,一般需要後臺做處理。

var express = require('express');
var app = express();

app.get('/', function (req, res) {
    res.send('welcome to home');
 }) 
var server = app.listen(8081)
複製程式碼

history-error

router的核心

hash路由

主要是監聽hashchange事件,然後再獲取資料重新渲染頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a href="#/a">pageALink</a>
    <a href="#/b">pageBLink</a>
    <span id='body'></span>
    <script>
        window.addEventListener('hashchange',(e)=>{
            document.getElementById('body').innerHTML = window.location
        },false)
    </script>
</body>
</html>
複製程式碼

history.push實現路由

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a onClick="go('/a')">pageALink</a>
    <a onClick="go('/b')">pageBLink</a>
    <span id='body'></span>
    <script>
        function go (pathname){
            window.history.pushState({},null,pathname);
            document.getElementById('body').innerHTML = window.location;
        }
        
        //pushState和replaceState是無法觸發popstate事件
        //這裡主要處理瀏覽器前進後退功能,不加下面的程式碼就無法實現前進後退功能
        window.addEventListener('popstate',(e)=>{
            let pathname = window.location;
            document.getElementById('body').innerHTML = window.location;
        })
    </script>
</body>
</html>
複製程式碼

react-router中原理分析

react-router的基礎構成

  1. BrowserRouter或hashRouter用來渲染Router所代表的元件
  2. Route用來匹配元件路徑並且篩選需要渲染的元件
  3. Switch用來篩選需要渲染的唯一元件
  4. Link直接渲染某個頁面元件
  5. Redirect類似於Link,在沒有Route匹配成功時觸發
import BrowserRouter from './BrowserRouter';
import Route from './Route';
import Link from './Link';
import Switch from './Switch';
import Redirect from './Redirect';
export {
  BrowserRouter,
  Route,
  Link,
  Switch,
  Redirect
}
複製程式碼

BrowserRouter

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import {BrowserRouter as Router,Route} from './react-router-dom'
render(<Router>
  <Render/>
  <div>
      <Route path="/" component={Home}>
      <Route path="/user" component={User}>
  </div>
</Router>,window.root);
複製程式碼

從上面的用法,可以知道BrowserRouter其實是一個元件,它有以下功能:

  1. 儲存當前的訪問的路徑,當路徑變化時會重新渲染Route所代表的元件
  2. 監聽popstate,路徑變化時會修改state儲存新的訪問路徑,從而重新渲染Route代表的元件
  3. 提供修改url和state的方法,供內部嵌入的元件使用,從而觸發頁面重新渲染
import React from 'react';
import {Provider} from './context';
// 
// 想染路徑變化 重新整理元件 路徑定義在狀態中 路徑變化就更新狀態
export default class BrowserRouter extends React.Component{
  state = {
    // 獲取開啟網頁時的預設路徑
    location:{
      pathname: window.location.pathname || '/',
    }
  }
  
  
  componentWillMount(){
    window.addEventListener('popstate',()=>{
      let pathname = window.location.pathname;
      this.handleChangeState(pathname);
    },false);
  }
  
  //當瀏覽器的路由改變時觸發,改變state從而重新渲染元件
  handleChangeState(pathname){
    this.setState({
      location:{
        ...this.state.location,
        pathname
      }
    })
  }
  
  // 渲染Route,
  render(){ 
    let that = this;
    let value = {
      ...this.state,
      history:{
        push(pathname){
          // 這個方法主要是提供給Link使用的
          // 當點選Link時,會改變瀏覽器url並且重新渲染元件
          window.history.pushState({},null,pathname);
          that.handleChangeState(pathname);
        }
      }
    }
    return( 
    <Provider value={value}>
        {this.props.children}   //嵌入的Route元件
    </Provider>
    )
  }
}
複製程式碼

Route

Route主要將所代表元件的path和當前的url(state.pathname)進行匹配,如果匹配成功則返回其代表的元件,那麼就會渲染其代表的元件,否則返回null。

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import {BrowserRouter as Router,Route} from './react-router-dom'
render(<Router>
  <Link to="/">首頁 </Link>
  
  /*由於Link不會有點選後的樣式變化,所以通常使用下面這用方法自定義link*/
  <Route path="/user" children={(match)=>{
    return <li><a className={match?'active':''}>使用者</a></li>}
  }
  <Render/>
  
  <div>
      <Route path="/" component={Home}>
      <Route path="/user" component={User}>
      /*採用render引數會執行對應的函式*/
      <Route path="/user" render={(props)=>{
        return <user/>
      }}/>
  </div>
</Router>,window.root);
複製程式碼
import React from 'react';
import {Consumer} from './context';
// 路徑轉化成正則,在另一篇文章【koa會用也會寫——(koa-router)】可以找到其原理
import pathToRegExp from 'path-to-regexp';

// 不是通過Route渲染出來的元件沒有match、location、history三個屬性
export default class Route extends React.Component{
  render(){
    return <Consumer>
      {(value)=>{
        // BrowserRouter中state.pathname和瀏覽器url一致
        let {pathname} = value.location; 
        
        // Route元件上的引數
        let {path='/',component:Component,render,children,exact=false} = this.props; 
        
        //用來儲存匹配路徑的引數鍵值 /user/:name/:id => [name,id]
        let keys = []; 
        
        //將Route的path引數轉化為正規表示式
        let reg = pathToRegExp(path,keys,{end:exact});
        
        
        if(reg.test(pathname)){
            let result = pathname.match(reg); 
            let match = {}
            
            // 將獲取路徑引數exp:{id:xxx,name:xxx}
            if(result){
              let [,...arr] = result;
              match.params = keys.reduce((memo,next,idx)=>{
                memo[keys[idx].name]=arr[idx]
                return memo;
              },{});
            }
            
            // 將匹配路徑的引數和原來的引數合併傳給Route代表的元件
            let props = {
                ...value,match
            }
            
            // component直接渲染元件
            // render執行render(props)
            // children不管是否匹配都會執行children(props)
            if(Component){
              return <Component {...props}></Component>
             }else if(render){
              return render(props);
             }else if(children){
              return render(props);
             }
        }else{
           // children 不管是否匹配到都會
           if(children){
              return render(props);
           }
           return null //Route的路徑不匹配返回null,不渲染Route代表的元件
        }
      }}
    </Consumer>
  }
}
複製程式碼

Switch

Switch元件其實就是包裝在Route外面的一層元件,它會對Route進行篩選後返回唯一Route,如果 沒有Switch的話,可以渲染多個Route代表的元件

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article';
import {BrowserRouter as Router,Route,Switch} from './react-router-dom'
render(<Router>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
    </Switch>
</Router>,window.root);
複製程式碼
import React from 'react';
import {Consumer} from './context';
import pathToRegExp from 'path-to-regexp';
export default class Switch extends React.Component{
  render(){
    return <Consumer>
      {(value)=>{
        // BrowserRouter中state.pathname和瀏覽器url一致
        let pathname = value.location.pathname;
        
        // 將Route的path對url進行匹配,匹配成功返回唯一的Route
        React.Children.forEach(this.props.children,(child)=>{
          let {path='/',exact=false} = child.props;
          let reg = pathToRegExp(path,[],{end:exact});
          if(reg.test(pathname)){
            return child    
          }
        })
      }}
    </Consumer>
  }
}
複製程式碼

Redirect

對於沒有匹配到的Route會預設重定向渲染Redirect,其實就是直接改變url和BrowserRouter中state.pathname導致重新渲染元件

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article';
import {BrowserRouter as Router,Route,Link,Switch,Redirect} from './react-router-dom'
render(<Router>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
      <Redirect to="/"/>
    </Switch>
</Router>,window.root);
複製程式碼
import React from 'react';
import {Consumer} from './context';
export default class Redirect extends React.Component{
  render(){
    return <Consumer>
      {({history})=>{   //修改url,重新渲染元件
          history.push(this.props.to);
          return null
      }}
    </Consumer>
  }
}
複製程式碼

Link

和Redirect元件類似,區別在於Redirect直接呼叫context上面的方法修改url,而Link需要點選觸發呼叫context上面的方法

import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article';
import {BrowserRouter as Router,Route,Link,Switch,Redirect} from './react-router-dom'
render(<Router>
  <Link to="/">首頁 </Link>
  <Link to="/user">使用者</Link>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
      <Redirect to="/"/>
    </Switch>
</Router>,window.root);
複製程式碼
import React from 'react';
import {Consumer} from './context';
export default class Link extends React.Component{
  render(){
    return <Consumer>
      {({history})=>{   //點選觸發回撥用,修改url,重新渲染元件
          return <a onClick={()=>{
            history.push(this.props.to)
          }}>{this.props.children}</a>
      }}
    </Consumer>
  }
}
複製程式碼

withRoute

不是通過Route渲染出來的元件沒有match、location、history三個屬性,但是又想要使用這三個屬性,那該怎麼辦呢,所以可以在外面套一層Route元件,從而得到這三個屬性,這種做法叫高階元件。

import React from 'react';
import Route from './Route'
let withRouter = (Component) =>{
  return ()=>{
    return <Route component={Component}></Route>
  }
}
export default withRouter;
複製程式碼
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom';
class withRouterLink extends Component {
  change = ()=>{
   this.props.history.push('/withRouterLink') // url變化,元件的跳轉
  }
  render() {
    return (
      <div className="navbar-brand" onClick={this.change}>withRouter</div>
    )
  }
}
// 高階元件
export default withRouter(Logo)
複製程式碼
import React from 'react';
import ReactDOM,{render} from 'react-dom';
import Home from './components/Home.js';
import User from './components/User.js';
import Article from './components/Article.js';
import withRouterLink from './components/withRouterLink.js';
import {BrowserRouter as Router,Route,Link,Switch,Redirect} from './react-router-dom'
render(<Router>
  <Link to="/">首頁 </Link>
  <Link to="/user">使用者</Link>
  <withRouterLink></withRouterLink>
  <Switch>
      <Route path="/" exact={true} component={Home}></Route>
      <Route path="/user" exact={true} component={User}></Route>
      <Route path="/article/:id" component={Article}/>
    </Switch>
</Router>,window.root);
複製程式碼

登陸攔截和登陸重回

一般網頁都會有登陸註冊功能,如果沒有登陸,很多頁面是訪問受限的,登陸之後又會跳轉到原頁面。

import Index from './pages/index.js';
import Protected from './pages/Protected'
export default class App extends Component {
  render() {
    return (
      <Router>
       <Index>
          <Switch>
            <Route path="/home" exact={true} component={Home}/>
            <Protected path="/profile" component={Profile}/>
            <Route path="/login" component={Login}/>
            <Redirect to="/home"/>
          </Switch>
       </Index>
      </Router>
    )
  }
}
複製程式碼
import React, { Component } from 'react'
import {Route,Redirect} from 'react-router-dom'
export default class Protected extends Component {
  render() {
   
    let login = localStorage.getItem('login');
    
    // this.props裡面有 path 有component
    //如果使用者沒有登入重定向到登入頁
    return login?<Route {...this.props}></Route>:<Redirect to={{pathname:"/login",state:{"from":'/profile'}}}/>
  }
}
複製程式碼
import React, { Component } from 'react'
export default class Login extends Component {
  render() {
    console.log(this.props)
    return (
      <div>
        <button onClick={()=>{
          // 通過引數識別 跳轉是否正確
          localStorage.setItem('login','ok');
          
          //拿到profile頁面跳轉到login頁面傳的from
          if(this.props.location.state){
            this.props.history.push(this.props.location.state.from);
          }else{
            this.props.history.push('/');
          }
        }} className="btn btn-danger">登入</button>
         <button onClick={()=>{
          localStorage.clear('login');
        }} className="btn btn-danger">退出</button>
      </div>
    )
  }
}
複製程式碼

結語

個人使用一種框架時總有一種想知道為啥這樣用的強迫症,不然用框架用的不舒服,不要求從原始碼上知道其原理,但是必須得從心理上說服自己。

相關文章