让react用起来更得心应手——(react-router原理简析)

7,312 阅读9分钟

让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
但是这种方法有一个问题,如果再按一次回车键,它是会向后台发送请求的,如果后台路由没有相应的匹配,那么会报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>
    )
  }
}

结语

个人使用一种框架时总有一种想知道为啥这样用的强迫症,不然用框架用的不舒服,不要求从源码上知道其原理,但是必须得从心理上说服自己。