讓react用起來更駕輕就熟——(react-router原理簡析)

讓react用起來更駕輕就熟系列文章:

  1. 讓react用起來更駕輕就熟——(react基礎簡析)
  2. 讓react用起來更駕輕就熟——(react-router原理簡析)
  3. 讓react用起來更駕輕就熟——(react-redux原理簡析)

前端路由和後臺路由

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

  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/ react

home
在瀏覽器中輸入localhost:8081/#a
#a
在瀏覽器中輸入localhost:8081/a
/a
結合圖片和上面的陳述應該知道前端路由和後臺路由的區別,除了hash路由還有一種方法能夠修改url而且不向後臺發送請求,它是history.pushState(),注意兼容處理:
history-push
history-result
可是這種方法有一個問題,若是再按一次回車鍵,它是會向後臺發送請求的,若是後臺路由沒有相應的匹配,那麼會報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實際上是一個組件,它有如下功能:express

  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。redux

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致使從新渲染組件bash

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組件,從而獲得這三個屬性,這種作法叫高階組件。markdown

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>
    )
  }
}
複製代碼

結語

我的使用一種框架時總有一種想知道爲啥這樣用的強迫症,否則用框架用的不舒服,不要求從源碼上知道其原理,可是必須得從心理上說服本身。

相關文章
相關標籤/搜索