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>
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.forward
、history.back
、和history.go
触发,因为这些操作有一个共性,即修改了历史堆栈的当前指针 - 在不改变document的前提下,一旦当前指针改变则会触发
onpopstate
事件
# 案例
- 浏览器针对每个页面维护一个
History
栈,执行pushState
函数可压入设定的url
至栈顶,同时修改当前指针 - 当执行
back
和forward
操作时,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>
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.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'));
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>
)
}
}
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": {}
}
}
*/
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>
)
}
}
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
}
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>
)
}
}
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;
}
}
}
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();
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'));
2
3
4
5
# 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'));
2
3
4
5
6
7
# 3.路径参数
let params = [];
let regx3 = pathToRegExp('/user/:id',params,{end:true});
console.log(regx3,params);
/**
/^\/user\/(?:([^\/]+?))\/?$/i
[ { name: 'id', optional: false, offset: 7 } ]
**/
2
3
4
5
6
7
# 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/));
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;
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;
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;
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';
2
3
4
# 实现Link
# 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'));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.Link.js
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>
)
}
}
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
}
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>
)
}
}
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'));
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'));
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
}
2
3
4
5
6
7
8
9
10
11
12
# 3.Link.js
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>
)
}
}
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;
}
}
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;
}
}
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;
}
}
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>
)
}
}
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>
)
}
}
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>
)
}
}
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>
)
}
}
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>
)
}
}
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;
}
}
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;
}
}
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'));
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;
}
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}}}/>
)
}/>
)
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>
)
}
}
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>
2
3
4
5
# 2.MenuLink.js
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>
)
}
/>
)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3.MenuLink.css
src/components/MenuLink.css
.navbar-inverse .navbar-nav > li > .active{
background-color: green!important;
color:red!important;
}
2
3
4
# 4.Link.js
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>
)
}
}
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;
+ }
}
}
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>
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)
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}>}/>
)
}
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
}
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>
)
}
}
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>
)
}
}
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
}
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;
}
}
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';
# 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>
)
}
}
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
}
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
2
3
4
5
6
7
8
9
10
11
12