react-router&dom自定义实现

# 简介

  • 不同的路径渲染不同的组件
  • 有两种实现方式
    • HashRouter:利用hash实现路由切换
    • BrowserRouter:实现h5 Api实现路由的切换

# 1.HashRouter

  • 利用hash实现路由切换

public\index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <title>React App</title>
</head>

<body>
  <div id="root"></div>
  <a href="#/a">去a</a>
  <a href="#/b">去b</a>

  <script>
    window.addEventListener('hashchange', () => {
      console.log(window.location.hash);
    });
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.BrowserRouter

  • 利用h5 Api实现路由的切换
  • HTML5规范给我们提供了一个history (opens new window)接口
  • HTML5 History API包括2个方法:history.pushState()history.replaceState(),和1个事件window.onpopstate

# 2.1.history

  • history (opens new window)对象提供了操作浏览器会话历史的接口。
  • history.length 属性声明了浏览器历史列表中的元素数量
  • pushState (opens new window) HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用
  • onpopstate (opens new window) window.onpopstate是popstate事件在window对象上的事件处理程序

# 2.2.pushState

  • pushState会往History中写入一个对象,他造成的结果便是,History.length +1、url 改变、该索引History对应有一个State对象,这个时候若是点击浏览器的后退,便会触发popstate事件,将刚刚的存入数据对象读出

  • pushState 会改变History

  • 每次使用时候会为该索引的State加入我们自定义数据

  • 每次我们会根据State的信息还原当前的view,于是用户点击后退便有了与浏览器后退前进一致的感受

  • pushState() 需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个URL

  • 调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法)

  • history.pushState(stateObject, title, url),包括三个参数

    • 第一个参数用于存储该url对应的状态对象,该对象可在onpopstate事件中获取,也可在history对象中获取
    • 第二个参数是标题,目前浏览器并未实现
    • 第三个参数则是设定的url
  • pushState函数向浏览器的历史堆栈压入一个url为设定值的记录,并改变历史堆栈的当前指针至栈顶

# 2.3.replaceState

  • 该接口与pushState参数相同,含义也相同
  • 唯一的区别在于replaceState是替换浏览器历史堆栈的当前历史记录为设定的url
  • 需要注意的是replaceState不会改动浏览器历史堆栈的当前指针

# 2.4.onpopstate

  • 该事件是window的属性
  • 该事件会在调用浏览器的前进、后退以及执行history.forwardhistory.back、和history.go触发,因为这些操作有一个共性,即修改了历史堆栈的当前指针
  • 在不改变document的前提下,一旦当前指针改变则会触发onpopstate事件

# 案例

  • 浏览器针对每个页面维护一个History栈,执行pushState函数可压入设定的url至栈顶,同时修改当前指针
  • 当执行backforward操作时,history栈大小并不会改变(history.length不变),仅仅移动当前指针的位置
  • 若当前指针在history栈的中间位置(非栈顶),此时执行pushState会在指针当前的位置添加此条目,并成为新的栈顶
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #root{
            border:1px solid red;
            height:20px;
        }
    </style>
</head>
<body>
    <div id="root"></div>
    <script>
        var historyObj = window.history;
        //监听路径改变事件 表示将当前的状态变更了,弹出了
        window.onpushstate = (event) => {
            console.log(event.type,event.detail.state);
            root.innerHTML = window.location.pathname;
        }
        window.addEventListener('popstate', (event) => {
            console.log(event.type,event.state);
            root.innerHTML = window.location.pathname;
        });

        (function (historyObj) {
            let oldPushState = history.pushState;//缓存原生的pushState
            historyObj.pushState = function (state, title, pathname) {
                let result = oldPushState.apply(history, arguments);
                if (typeof window.onpushstate === 'function') {
                    window.onpushstate(new CustomEvent('pushstate',{detail:{pathname,state}}));
                }
                return result;
            }
        })(historyObj);
        let oldHistoryLength = historyObj.length;
        setTimeout(() => {
            historyObj.pushState({ page: 1 }, { title: 'page1' }, '/page1');//page1
            console.log(historyObj.length-oldHistoryLength);
        }, 1000);
        setTimeout(() => {
            historyObj.pushState({ page: 2 }, { title: 'page2' }, '/page2');//page2
            console.log(historyObj.length-oldHistoryLength);
        }, 2000);
        setTimeout(() => {
            historyObj.pushState({ page: 3 }, { title: 'page3' }, '/page3');//page3
            console.log(historyObj.length-oldHistoryLength);
        }, 3000);
        setTimeout(() => {
            historyObj.back();//historyObj.go(-1);//page2
            setTimeout(()=>console.log(historyObj.length-oldHistoryLength),100);

        }, 4000);
        setTimeout(() => {
            historyObj.pushState({ page:4 }, { title: 'page4' }, '/page4');//page4
            console.log(historyObj.length-oldHistoryLength);
        }, 5000);
        setTimeout(() => {
            historyObj.go(1);
            console.log(historyObj.length-oldHistoryLength);//page4
        }, 6000);
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 基本用法

npm i react-router-dom @types/react-router-dom path-to-regexp -S
1

routeprops

# 1.src\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route} from 'react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
    <Router>
        <div>
          <Route path="/" component={Home} />
          <Route path="/user" component={User} />
          <Route path="/profile" component={Profile}/>
        </div>
    </Router>
,document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.components\Home.js

src\components\Home.js

import React,{Component} from 'react';
export default class Home extends Component{
    render() {
        return (
            <div>Home</div>
        )
    }
}
1
2
3
4
5
6
7
8

# 3.components\User.js

src\components\User.js

import React,{Component} from 'react';
export default class User extends Component{
    render() {
        console.log(JSON.stringify(this.props));
        return (
            <div>User</div>
        )
    }
}

/**
{
    "history": {
        "length": 4,
        "action": "POP",
        "location": {
            "pathname": "/user",
            "search": "?name=zfpx",
            "hash": "#top"
        }
    },
    "location": {
        "pathname": "/user",
        "search": "?name=zfpx",
        "hash": "#top"
    },
    "match": {
        "path": "/user",
        "url": "/user",
        "isExact": true,
        "params": {}
    }
}
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 4.components\Profile.js

src\components\Profile.js

import React,{Component} from 'react';
export default class Profile extends Component{
    render() {
        return (
            <div>Profile</div>
        )
    }
}
1
2
3
4
5
6
7
8

# 实现路由

# 1.index.js

src\react-router-dom\index.js

import HashRouter from './HashRouter';
import Route from './Route';
export {
    HashRouter,
    Route
}
1
2
3
4
5
6

# 2.HashRouter.js

src\react-router-dom\HashRouter.js

import React, { Component } from 'react'
import Context from './context';
export default class HashRouter extends Component {
  state = {
      location:{pathname:window.location.hash.slice(1)}
  }  
  componentWillMount(){
      window.addEventListener('hashchange',()=>{
          this.setState({
              location:{
                  ...this.state.location,
                  pathname:window.location.hash.slice(1) || '/'
              }
          });
      });
      window.location.hash = window.location.hash || '/';
  }
  render() {
    let value = {
        location:this.state.location
    }
    return (
      <Context.Provider value={value}>
        {this.props.children}
      </Context.Provider>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 3.Route.js

src\react-router-dom\Route.js

import React,{Component} from 'react';
import Context from './context';
export default class Route extends Component{
    static contextType =  Context;  
    render() {
        let {path,component: Component}=this.props;
        let pathname = this.context.location.pathname;
        if (path == pathname) {
            return <Component/>
        } else {
            return null;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.context.js

src\react-router-dom\context.js

import {createContext} from 'react';
export default  createContext();
1
2

# path-to-regexp

# 1./home结束

let pathToRegExp = require('path-to-regexp');
let regx = pathToRegExp('/home',[],{end:true});
console.log(regx);//   /^\/home\/?$/i
console.log(regx.test('/home'));
console.log(regx.test('/home/2'));
1
2
3
4
5

homereg

# 2./home非结束

let pathToRegExp = require('path-to-regexp');
let regx2 = pathToRegExp('/home',[],{end:false});
console.log(regx2);//   /^\/home\/?(?=\/|$)/i
console.log(regx2.test('/home'));
console.log(regx2.test('/home/'));
console.log(regx2.test('/home//'));
console.log(regx2.test('/home/2'));
1
2
3
4
5
6
7

homereg2

# 3.路径参数

let params = [];
let regx3 = pathToRegExp('/user/:id',params,{end:true});
console.log(regx3,params);
/**
/^\/user\/(?:([^\/]+?))\/?$/i
[ { name: 'id', optional: false, offset: 7 } ]
**/
1
2
3
4
5
6
7

uerreg

# 4.正则匹配

表达式 含义
() 表示捕获分组,()会把每个分组里的匹配的值保存起来,使用$n(n是一个数字,表示第n个捕获组的内容)
(?😃 表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来
  • 正则匹配的前瞻就是给正则匹配的选项定义一个断言,或者说是一个条件比如:我要匹配一个字母,但是我的需求是字母后面必须是跟着一个数字的情况,那么这种场景是怎么实现了,就是用到前瞻的概念,那么我想要他的前面也要是一个数字怎么办了,这就是后顾
表达式 含义
(?=pattern) 正向肯定查找(前瞻),后面必须跟着什么
(?!pattern) 正向否定查找(前瞻),后面不能跟着什么
(?<=pattern) 反向肯定条件查找(后顾),不捕获
(?<!pattern) 反向否定条件查找(后顾)
console.log('1a'.match(/\d(?=[a-z])/));
console.log('1@'.match(/\d(?![a-z])/));
console.log('a1'.match(/(?<=[a-z])\d/));
console.log('$1'.match(/(?<![a-z])\d/));
1
2
3
4

# 正则匹配

# 1.matchPath.js

src\react-router\matchPath.js

import pathToRegexp from 'path-to-regexp';
function compilePath(path,options){
    const keys = [];
    const regexp = pathToRegexp(path,keys,options);
    return {keys,regexp};
}
/**
 * @param {*} pathname 浏览器栏中的真实路径
 * @param {*} options 匹配的参数 path exact strict sensitive
 */
function matchPath(pathname,options = {}){
    let {path='/',exact=false,strict=false,sensitive=false}=options;
    let {keys,regexp} = compilePath(path,{
        end:exact,
        strict,
        sensitive
    }); // /post/:id  keys=["id"] regexp= /\/post\/([^\/]+?)/
    const match = regexp.exec(pathname);
    if(!match) return null;
    const [url,...values] = match;//['/post/1','1'] url=/post/1 values=['1']
    // pathname /post/1/name !== /post/1
    const isExact = pathname === url;
    //需要精确匹配,但是匹配的不精确,没有完全相等,也相当于没匹配上
    if(exact && !isExact) return null;
    return { //路由组件中props.match
        path,//Route原始path
        url,//正则匹配到的浏览器的pathname的部分
        isExact,
        params:keys.reduce((memo,key,index)=>{
            memo[key.name] = values[index];
            return memo;
        },{})
    }
}

export default matchPath;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 2.Router.js

src\react-router\Router.js

import React from 'react'
import RouterContext from './RouterContext';
class Router extends React.Component{
+    static computeRootMatch(pathname) {
+        return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
+    }
    constructor(props){
        super(props);
        this.state = {
            location:props.history.location
        }
        //当路径发生的变化的时候执行回调
        this.unlisten = props.history.listen((location)=>{
            this.setState({location});
        });
    }
    componentWillUnmount(){
        this.unlisten&&this.unlisten();
    }
    render(){
        let value = {//通过value向下层传递数据
            location:this.state.location,
            history:this.props.history,
+            match: Router.computeRootMatch(this.state.location.pathname)
        }
        return (
            <RouterContext.Provider value={value}>
                {this.props.children}
            </RouterContext.Provider>
        )
    }
}

export default Router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 3.Route.js

src\react-router\Route.js

import React from 'react'
import RouterContext from './RouterContext';
+import matchPath from './matchPath';
class Route extends React.Component{
    static contextType = RouterContext
    render(){
        const {history,location} = this.context;
        const {component:RouteComponent} = this.props;
+        const match = matchPath(location.pathname, this.props);
        let routeProps = {history,location};
        let element=null;
        if (match) {
+           routeProps.match = match;
            element = <RouteComponent {...routeProps} />
        }
        return (
            <RouterContext.Provider value={routeProps}>
                {element}
            </RouterContext.Provider>
        )
    }
}
export default Route;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 4.react-router\index.js

src\react-router\index.js

export {default as Route} from './Route';
export {default as Router} from './Router';
export {default as __RouterContext} from './RouterContext';
+export {default as matchPath} from './matchPath';
1
2
3
4

# 1.src/index.js

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route,Link} from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
    <Router>
        <>
          <Link to="/">Home</Link><Link to="/user">User</Link><Link to="/profile">Profile</Link>
          <Route path="/" exact component={Home} />
          <Route path="/user" component={User} />
          <Route path="/profile" component={Profile}/>
        </>
    </Router>
,document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

src/react-router-dom/Link.js

import React, { Component } from 'react'
import Context from './context';
export default class Link extends Component {
  static contextType =  Context;  
  render() {
    return (
        <a onClick={()=>this.context.history.push(this.props.to)}>{this.props.children}</a>
    )
  }
}
1
2
3
4
5
6
7
8
9
10

# 3.index.js

src/react-router-dom/index.js

import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
export {
    HashRouter,
    Route,
    Link
}
1
2
3
4
5
6
7
8

# 4.HashRouter.js

src/react-router-dom/HashRouter.js

import React, { Component } from 'react'
import Context from './context';
export default class HashRouter extends Component {
  state = {
      location:{pathname:window.location.hash.slice(1) || '/'},
  }  
  componentDidMount(){
      window.location.hash = window.location.hash || '/';
      window.addEventListener('hashchange',()=>{
          this.setState({
              location:{
                  ...this.state.location,
                  pathname:window.location.hash.slice(1) || '/'
              }
          });
      });
  }
  render() {
     let value={
            location: this.state.location,
            history: {
                push(to) {
                    window.location.hash=to;
                }
            }
    }
    return (
      <Context.Provider value={value}>
        {this.props.children}
      </Context.Provider>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 引入bootstrap

# 1.src\index.js

src\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route,Link} from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
    <Router>
        <>
          <div className="navbar navbar-inverse">
            <div className="container-fluid">
              <div className="navbar-heading">
                  <div className="navbar-brand">珠峰架构</div>
              </div>
              <ul className="nav navbar-nav">
                <li><Link to="/">Home</Link></li>
                <li><Link to="/user">User</Link></li>
                <li><Link to="/profile">Profile</Link></li>
              </ul>
            </div>
          </div>
          <div className="container">
            <div className="row">
              <div className="col-md-12">
                <Route path="/" exact component={Home} />
                <Route path="/user" component={User} />
                <Route path="/profile" component={Profile}/>
              </div>
            </div>
          </div>
        </>
    </Router>
,document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# Redirect&Switch

# 1.src\index.js

src\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route,Link,Redirect,Switch} from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
    <Router>
        <>
          <div className="navbar navbar-inverse">
            <div className="container-fluid">
              <div className="navbar-heading">
                  <div className="navbar-brand">珠峰架构</div>
              </div>
              <ul className="nav navbar-nav">
                <li><Link to="/">Home</Link></li>
                <li><Link to="/user">User</Link></li>
                <li><Link to="/profile">Profile</Link></li>
              </ul>
            </div>
          </div>
          <div className="container">
            <div className="row">
              <div className="col-md-12">
                <Switch>
                  <Route path="/" exact component={Home} />
                  <Route path="/user" component={User} />
                  <Route path="/profile" component={Profile}/>
                  <Redirect to="/" />
                </Switch>
              </div>
            </div>
          </div>
        </>
    </Router>
,document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 2.index.js

src/react-router-dom/index.js

import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
import Redirect from './Redirect';
import Switch from './Switch';
export {
    HashRouter,
    Route,
    Link,
    Redirect,
    Switch
}
1
2
3
4
5
6
7
8
9
10
11
12

src/react-router-dom/Link.js

import React, { Component } from 'react'
import Context from './context';
export default class Link extends Component {
  static contextType =  Context;  
  render() {
    return (
        <a onClick={()=>this.context.history.push(this.props.to)}>{this.props.children}</a>
    )
  }
}
1
2
3
4
5
6
7
8
9
10

# 4.Route.js

src/react-router-dom/Route.js

import React, { Component } from 'react'
import Context from './context';
import pathToRegexp from 'path-to-regexp';
export default class Route extends Component {
  static contextType =  Context;  
  render() {
    let {path,component:Component,exact=false} = this.props;
    let pathname = this.context.location.pathname;
    let regxp = pathToRegexp(path,[],{end:exact});
    let result = pathname.match(regxp);
    if(result){
        return <Component {...this.context}/>;
    }
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.Redirect.js

src/react-router-dom/Redirect.js

import React, { Component } from 'react'
import Context from './context';
export default class Link extends Component {
  static contextType = Context;  
  render() {
    this.context.history.push(this.props.to);  
    return null;
  }
}
1
2
3
4
5
6
7
8
9

# 6.Switch.js

src/react-router-dom/Switch.js

import React, { Component } from 'react'
import Context from './context';
import pathToRegexp from 'path-to-regexp';
export default class Switch extends Component {
  static contextType =  Context;  
  render() {
    let pathname = this.context.location.pathname;
    for(let i=0;i<this.props.children.length;i++){
        let child = this.props.children[i];
        let {path='/',component:Component,exact=false} = child.props;
        let regxp = pathToRegexp(path,[],{end:exact});
        let result = pathname.match(regxp);
        if(result){
            return child;
        }
    }
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 路径参数

# 1.User.js

src/components/User.js

import React,{Component} from 'react';
import {Link,Route} from '../react-router-dom';
import UserAdd from './UserAdd';
import UserDetail from './UserDetail';
import UserList from './UserList';
export default class User extends Component{
    render() {
        return (
            <div className="row">
               <div className="col-md-2">
                       <ul className="nav nav-stack">
                        <li><Link to="/user/list">用户列表</Link></li>    
                        <li><Link to="/user/add">添加用户</Link></li>    
                    </ul>
               </div>
               <div className="col-md-10">
                       <Route path="/user/add" component={UserAdd}/>
                    <Route path="/user/list" component={UserList}/>
                    <Route path="/user/detail/:id" component={UserDetail}/>
               </div>
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2.UserAdd.js

src/components/UserAdd.js

import React, { Component } from 'react'

export default class UserAdd extends Component {
  constructor(){
    super();
    this.usernameRef = React.createRef();
  }
  handleSubmit = (event)=>{
    event.preventDefault();
    let username = this.usernameRef.current.value;
    let usersStr = localStorage.getItem('users');
    let users = usersStr?JSON.parse(usersStr):[];
    users.push({id:Date.now()+'',username});
    localStorage.setItem('users',JSON.stringify(users));
    this.props.history.push('/user/list');
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input className="form-control" type="text" ref={this.usernameRef}/>
        <button type="submit" className="btn btn-primary">提交</button>
      </form>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 3.UserList.js

src/components/UserList.js

import React, { Component } from 'react'
import {Link} from '../react-router-dom';
export default class UserList extends Component {
  state = {
      users:[]
  }  
  componentDidMount(){
    let usersStr = localStorage.getItem('users');
    let users = usersStr?JSON.parse(usersStr):[];
    this.setState({users});
  }
  render() {
    return (
      <div>
        UserList
        <ul className="list-group">
        {
            this.state.users.map((user,index)=>(
                <li className="list-group-item" key={index}>
                 <Link to={{pathname:`/user/detail/${user.id}`,state :user}}>{user.username}</Link>
                </li>    
            ))
        }    
        </ul>
      </div>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 3.UserDetail.js

src/components/UserDetail.js

import React, { Component } from 'react'

export default class UserDetail extends Component {
  state = {
    user:{}
  }  
  componentDidMount(){
    let user = this.props.location.state;
    if(!user){
      let usersStr = localStorage.getItem('users');
      let users = usersStr?JSON.parse(usersStr):[];
      let id = this.props.match.params.id;
      user = users.find(user=>user.id === id);
    }
    if(user) this.setState({user});
  }
  render() {
    let user = this.state.user;
    return (
      <div>
        {user.id}:{user.username}
      </div>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 4.HashRouter.js

src/react-router-dom/HashRouter.js

import React, { Component } from 'react'
import Context from './context';
export default class HashRouter extends Component {
  state = {
      location:{pathname:window.location.hash.slice(1) || '/'}
  }  
  locationState=undefined
  componentDidMount(){
      window.location.hash = window.location.hash || '/';
      window.addEventListener('hashchange',()=>{
          this.setState({
              location:{
                  ...this.state.location,
                  pathname:window.location.hash.slice(1) || '/',
                  state:this.locationState
              }
          });
      });
  }
  render() {
    let that = this;
    let value = {
      location:that.state.location,
      history:{
        push(to){
          if(typeof to === 'object'){
            let {pathname,state}= to;
            that.locationState = state;
            window.location.hash = pathname; 
          }else{
            window.location.hash = to;
          }  
      }
      }
    }
    return (
      <Context.Provider value={value}>
        {this.props.children}
      </Context.Provider>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 5.Route.js

src/react-router-dom/Route.js

import React, { Component } from 'react'
import Context from './context';
import pathToRegexp from 'path-to-regexp';
export default class Route extends Component {
  static contextType = Context;
  render() {
    let { path, component: Component, exact = false } = this.props;
    let pathname = this.context.location.pathname;
    let keys = [];
    let regxp = pathToRegexp(path, keys, { end: exact });
    let result = pathname.match(regxp);
    if (result) {
      let [url, ...values] = result;
      keys = keys.map(item=>item.name);
      let params = values.reduce((memo, val, index) => {
        memo[keys[index]] = val;
        return memo;
      }, {});
      let match = {
        url:pathname,
        isExact:pathname===url,
        path,
        params
      }
      let props = {
        location: this.context.location,
        history: this.context.history,
        match
      }
      return <Component {...props} />;
    }
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 6.Switch.js

src/react-router-dom/Switch.js

import React, { Component } from 'react'
import Context from './context';
import pathToRegexp from 'path-to-regexp';
export default class Switch extends Component {
  static contextType =  Context;  
  render() {
    let pathname = this.context.location.pathname;
    let children = Array.isArray(this.props.children)?this.props.children:[this.props.children]
    for(let i=0;i<children.length;i++){
        let child = children[i];
        let {path='/',exact=false} = child.props;
        let regxp = pathToRegexp(path,[],{end:exact});
        let result = pathname.match(regxp);
        if(result){
            return child;
        }
    }
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 受保护的路由

# 1.src/index.js

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route,Link,Redirect,Switch} from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import Protected from './components/Protected';
import Login from './components/Login';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
    <Router>
        <>
          <div className="navbar navbar-inverse">
            <div className="container-fluid">
              <div className="navbar-heading">
                  <div className="navbar-brand">珠峰架构</div>
              </div>
              <ul className="nav navbar-nav">
                <li><Link to="/">Home</Link></li>
                <li><Link to="/user">User</Link></li>
                <li><Link to="/profile">Profile</Link></li>
              </ul>
            </div>
          </div>
          <div className="container">
            <div className="row">
              <div className="col-md-12">
                <Switch>
                  <Route path="/" exact component={Home} />
                  <Route path="/user" component={User} />
                  <Route path="/login" component={Login} />
                  <Protected path="/profile" component={Profile}/>
                  <Redirect to="/" />
                </Switch>
              </div>
            </div>
          </div>
        </>
    </Router>
,document.getElementById('root'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 2.Route.js

src/react-router-dom/Route.js

      let props = {
        location: this.context.location,
        history: this.context.history,
        match
      }
      if(Component){
        return <Component {...props} />;
      }else if(render){
        return render(props);
      }else{
        return null;
      }
1
2
3
4
5
6
7
8
9
10
11
12

# 3.Protected.js

src/components/Protected.js

import React, { Component } from 'react'
import {Route,Redirect} from '../react-router-dom';
export default ({component:Component,...rest})=>(
    <Route {...rest} render={
        props => (
            localStorage.getItem('logined')?<Component {...props}/>:<Redirect to={{pathname:'/login',state:{from:props.location.pathname}}}/>
        )
    }/>
)
1
2
3
4
5
6
7
8
9

# 4.Login.js

src/components/Login.js

import React, { Component } from 'react'

export default class Login extends Component {
  handleClick = ()=>{
    localStorage.setItem('logined','true');
    if(this.props.location.state)
      this.props.history.push(this.props.location.state.from);
  }  
  render() {
    return (
      <button className="btn btn-primary" onClick={this.handleClick}>登录</button>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 自定义导航

# 1.index.js

src/index.js

<ul className="nav navbar-nav">
+    <li><MenuLink exact to="/">Home</MenuLink></li>
+    <li><MenuLink to="/user">User</MenuLink></li>
+    <li><MenuLink to="/profile">Profile</MenuLink></li>
</ul>
1
2
3
4
5

src/components/MenuLink.js

import React from 'react'
import {Route,Link} from '../react-router-dom';
import './MenuLink.css'
export default ({to,exact,children}) => (
    <Route
        path={to}
        exact={exact}
        children={
            props => (
                <Link className={props.match?'active':''} to={to}>{children}</Link>
            )
        }
    />
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

src/components/MenuLink.css

.navbar-inverse .navbar-nav > li > .active{
    background-color: green!important;
    color:red!important;
}
1
2
3
4

src\react-router-dom\Link.js

import React, { Component } from 'react'
import Context from './context';
export default class Link extends Component {
  static contextType =  Context;  
  render() {
    return (
+        <a {...this.props} onClick={()=>this.context.history.push(this.props.to)}>{this.props.children}</a>
    )
  }
}
1
2
3
4
5
6
7
8
9
10

# 5.Route.js

src/react-router-dom/Route.js

import React, { Component } from 'react'
import Context from './context';
import pathToRegexp from 'path-to-regexp';
export default class Route extends Component {
  static contextType = Context;
  render() {
    let { path='/', component: Component, exact = false,render,children } = this.props;
    let pathname = this.context.location.pathname;
    let keys = [];
    let regxp = pathToRegexp(path, keys, { end: exact });
    let result = pathname.match(regxp);
+    let props = {
+      location: this.context.location,
+      history: this.context.history
+    }
    if (result) {
      let [url, ...values] = result;
      keys = keys.map(item=>item.name);
      let params = values.reduce((memo, val, index) => {
        memo[keys[index]] = val;
        return memo;
      }, {});
      let match = {
        url:pathname,
        isExact:pathname===url,
        path,
        params
      }
+      props.match = match;
      if(Component){
        return <Component {...props} />;
      }else if(render){
        return render(props);
+      }else if(children){
+        return children(props);
+      }else{
+        return null;
+      }
+    }else if(children){
+      return children(props);
+    }else{
+      return null;
+    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# withRouter

# 1.src/index.js

+import NavHeader from './components/NavHeader';
<div className="navbar navbar-inverse">
            <div className="container-fluid">
+              <NavHeader/>
              <ul className="nav navbar-nav">
                  <MenuLink exact to="/">Home</MenuLink>
                  <MenuLink to="/user">User</MenuLink>
                  <MenuLink to="/profile">Profile</MenuLink>
              </ul>
</div>
1
2
3
4
5
6
7
8
9
10

# 2.NavHeader.js

src/components/NavHeader.js

import React, { Component } from 'react'
import {withRouter} from '../react-router-dom';
class NavHeader extends Component {
  render() {
    return (
        <div className="navbar-heading">
            <div className="navbar-brand" onClick={()=>this.props.history.push('/')}>珠峰架构</div>
        </div>
    )
  }
}
export default withRouter(NavHeader)
1
2
3
4
5
6
7
8
9
10
11
12

# 3.withRouter.js

src/react-router-dom/withRouter.js

import React from 'react'
import {Route} from '../react-router-dom';
export default function (Component) {
    return props=>(
        <Route render={routeProps=><Component {...routeProps}>}/>
    )
}
1
2
3
4
5
6
7

# 4.index.js

src/react-router-dom/index.js

import withRouter from './withRouter';
export {
    HashRouter,
    Route,
    Link,
    Redirect,
    Switch,
    withRouter
}
1
2
3
4
5
6
7
8
9

# 阻止跳转

# 1.UserAdd.js

src/components/UserAdd.js

import React, { Component } from 'react'
import {Prompt} from '../react-router-dom';
export default class UserAdd extends Component {
  constructor(){
    super();
    this.usernameRef = React.createRef();
  }
  state = {
    isBlocking:false
  }
  handleSubmit = (event)=>{
    event.preventDefault();
    this.setState({
      isBlocking:false
    },()=>{
      let username = this.usernameRef.current.value;
      let usersStr = localStorage.getItem('users');
      let users = usersStr?JSON.parse(usersStr):[];
      users.push({id:Date.now()+'',username});
      localStorage.setItem('users',JSON.stringify(users));
      this.props.history.push('/user/list');
    })

  }
  render() {
    let {isBlocking}=this.state;
    return (
      <form onSubmit={this.handleSubmit}>
       <Prompt
              when={isBlocking}
              message={location=>`你确定要跳转到${location.pathname}吗?`}
        />
        <input className="form-control" type="text" 
           onChange={event => {
            this.setState({isBlocking:event.target.value.length>0});
           }}
           ref={this.usernameRef}/>
        <button type="submit" className="btn btn-primary">提交</button>
      </form>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 2.HashRouter.js

src/react-router-dom/HashRouter.js

import React, { Component } from 'react'
import Context from './context';
export default class HashRouter extends Component {
  state = {
    location: { pathname: window.location.hash.slice(1) || '/' }
  }
  locationState = undefined
  getMessage=null
  componentDidMount() {
    window.location.hash = window.location.hash || '/';
    window.addEventListener('hashchange', () => {
      this.setState({
        location: {
          ...this.state.location,
          pathname: window.location.hash.slice(1) || '/',
          state: this.locationState
        }
      });
    });
  }
  render() {
    let that = this;
    let value = {
      location: that.state.location,
      history: {
        push(to) {
          if (that.getMessage) {
              let allow=window.confirm(that.getMessage(that.state.location)+`,并且跳转到${typeof to === 'object'?to.pathname:to}吗?`);
              if (!allow) return;
          }
          if (typeof to === 'object') {
            let { pathname, state } = to;
            that.locationState = state;
            window.location.hash = pathname;
          } else {
            window.location.hash = to;
          }
        },
        block(message) {
          that.getMessage=message;
        },
        unblock() {
          that.getMessage=null;
        }
      }
    }
    return (
      <Context.Provider value={value}>
        {this.props.children}
      </Context.Provider>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 3.index.js

src/react-router-dom/index.js

import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
import Redirect from './Redirect';
import Switch from './Switch';
import withRouter from './withRouter';
import Prompt from './Prompt';
export {
    HashRouter,
    Route,
    Link,
    Redirect,
    Switch,
    withRouter,
    Prompt
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.Prompt.js

src/react-router-dom/Prompt.js

import React from 'react'
import Context from './context';
export default class Prompt extends React.Component{
    static contextType = Context;
    componentWillUnmount() {
        this.history.unblock();
    }
    render() {
        this.history=this.context.history;
        const {when,message}=this.props;
        if (when) {
            this.history.block(message);
        } else {
            this.history.block(null);
        }
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# BrowserRouter

# 1.index.js

src/index.js

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

# 2.BrowserRouter.js

src/react-router-dom/BrowserRouter.js

import React, { Component } from 'react'
import Context from './context';
(function (history) {
    var pushState = history.pushState;
    history.pushState = function (state,title,pathname) {
        if (typeof window.onpushstate == "function") {
            window.onpushstate(state,pathname);
        }
        return pushState.apply(history, arguments);
    };
})(window.history);
export default class BrowserRouter extends Component {
    state = {
        location: { pathname: '/' }
    }
    getMessage = null
    componentDidMount() {
        window.onpopstate = (event) => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname:document.location.pathname,
                    state:event.state
                }
            });
        };
        window.onpushstate = (state,pathname) => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname,
                    state
                }
            });
        };
    }
    render() {
        let that = this;
        let value = {
            location: that.state.location,
            history: {
                push(to) {
                    if (that.block) {
                        let allow = window.confirm(that.getMessage(that.state.location));
                        if (!allow) return;
                    }
                    if (typeof to === 'object') {
                        let { pathname, state } = to;
                        window.history.pushState(state, '', pathname);
                    } else {
                        window.history.pushState('', '', to);
                    }
                },
                block(getMessage) {
                    that.block = getMessage;
                },
                unblock() {
                    that.block = null;
                }
            }
        }
        return (
            <Context.Provider value={value}>
                {this.props.children}
            </Context.Provider>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 3.index.js

src/react-router-dom/index.js

import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
import Redirect from './Redirect';
import Switch from './Switch';
import withRouter from './withRouter';
import Prompt from './Prompt';
import BrowserRouter from './BrowserRouter';

export {
    HashRouter,
    Route,
    Link,
    Redirect,
    Switch,
    withRouter,
    Prompt,
    BrowserRouter
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 参考

# 可选参数

let express = require("express");
let app = express();
/* app.get('/member/?:path/?:tag', (req, res) => {
    res.json(req.params);
}); */
// /^\/member\/?(?:([^\/]+?))\/?(?:([^\/]+?))\/?$/i
let reg = /^\/member\/([^\/]+?)?(?:\/([^\/]+?))?\/?$/;
app.get(reg, (req, res) => {
    res.json({ path: req.params[0], tag: req.params[1] });
});
app.listen(9999);
//  /member/path
1
2
3
4
5
6
7
8
9
10
11
12
上次更新: 2022/04/15, 05:41:29
×