测试用例
import React from "react";
import ReactDOM from "react-dom";
import { HashRouter as Router, Route } from "react-router-dom";
let Home = () => <div>Home</div>;
let User = () => <div>User</div>;
let Profile = () => <div>Profile</div>;
function App() {
return (
<Router>
<div>
<Route path="/home" component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</div>
</Router>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
现在我们来开始写react-router-dom
我们先看看看
let Home = (...rest) => {
console.log(rest);
return <div>Home</div>;
};
这里的参数就是
(props,context)

然后我们测试每个Route渲染的组件,都会发现它的porps上有以上三个对象

这里我们主要实现个别几个属性
- history
- push
- location
- patchname
- match
- params
- path
- url
这些数据都是父组建传递给子组建的
即这些属性是HashRouter传递给Route的
那我们就思考下怎么实现这种东西的传递?
代码实现
首先我们创建一个src/react-router-dom文件夹
然后创建一个index.js文件来统一导出组件
import HashRouter from './HashRouter'
import Route from './Route'
export { HashRouter, Route }
现在开始实现代码实现
因为Route是可以在Router内部的任意位置的
那么作为Router就应该通过上下文来传递属性
于是我们先创建一个上下文
在老版 Context API 中,“上下文”只是一个概念,并不对应一个代码,两个组件之间达成一个协议,就诞生了“上下文”。
在新版 Context API 中,需要一个“上下文”对象,上下文数据的提供或者被消费的代码很大概率是分布在不同文件的
所以为了方便各文件调用上下文,我们单独使用一个文件夹来创建,需要使用该上下文的时候,直接引入该文件即可
context.js
import { createContext } from 'react';
export default createContext();
Route.js
接下来我们来实现Route.js
HashRouter应该作为一个提供者,为每个消费者Route提供我们上面说的三个属性
- history
- location
- match
那对于消费者的Route来说
他的作用就是
<Route path="/home" component={Home} />
如果当前的hash与path匹配,那么就把Home渲染出来,即
render(){
if(hash == path)
return <Home/>
else
return null
}
那问题来了,我们怎么获取的hash呢?
这个hash就是作为提供者的HashRouter给予的
而他是通过上下文提供的,这里我们假装HashRouter已经准备好了数据
那我们先在函数里声明我们要使用到的上下文对象
import React,{Component} from 'react';
import Context from './context';
export default class Route extends Component{
static contextType = Context;
}
那现在开始,我们就可以使用this.context来获取上下文数据了
let pathname = this.context.location.pathname;
这个数据就是我们前面说到的hash
那现在轮到我们获取Route组建上的属性了
let {path,component: Component}=this.props;
这里我们给component重命名为Component,因为到时这个属性会作为组件渲染
所以需要首字母大写,即<Component/>
那判断逻辑就
if (path === pathname) {
return <Component/>
} else {
return null;
}
综上,代码就是
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;
}
}
}
HashRouter.js
我们在前面,已经假装使用了HashRouter已经实现的数据,即
let pathname = this.context.location.pathname;
在实现该数据前,我们思考下HashRouter的大致架构
import React from "react";
import { HashContext } from "./HashContext";
export default class HashRouter extends React.Component {
render() {
let value = {
location
}
return (
<HashContext.Provider value={value}>
{this.props.children}
</HashContext.Provider>
);
}
}
上面代码主要的作用有两个
- 渲染
HashRouter,即{this.props.children} - 为子孙组件提供上下文,即
<HashContext.Provider value={value}>
而刚刚Route要使用的数据就应该添加到value然后传递出去
我们现在来思考如何实现location.pathname
获取hahs的办法有
window.location.hash
当url为http://localhost:3000/#/home时
这个值为#/home,而我们想要的数据是/home,所以我们对返回的结果分割下
window.location.hash.slice(1)
那这个数据应该存储在哪呢?
我们要的效果是,当浏览器durl发生变化时
相应的路由也要发生变化
如果提供者的value值发生变化了,那么订阅该上下文的组件也会重新渲染
所以我们应该把数据放在this.state上
然后再添加一个监听函数,当url发生变化时我们就修改this.state
就让就会触发提供者的value发生变化,从而整个页面发生变化
就有
state = {
location: { pathname: window.location.hash.slice(1) }
}
那监听函数应该放在didmount上
这里我们还要给url赋一个默认值,因为我们这里统一是使用hash的
即开始url就应该是http://localhost:3000/#/
这样我们才方便在后面添加地址
componentDidMount() {
window.location.hash = window.location.hash || '/';
}
如果没有这句话,我们登录页面的地址http://localhost:3000/
这样的地方不方便我们直接在后面添加user之类的字符串
因为如果直接添加user,地址是http://localhost:3000/user这与我们的路由是不匹配的
我们的路由地址实际上是http://localhost:3000/#/user
然后现在我们要开始监听hash变化了
window.addEventListener('hashchange', () => { });
一旦发生了变化就会执行后面的回调函数
该回调函数里,我们应该更改this.state的i值,所以有
window.addEventListener('hashchange', () => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
});
});
那我们给提供者的value就是this.state了
综上,代码就是
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.addEventListener('hashchange', () => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
});
});
window.location.hash = window.location.hash || '/';
}
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
测试用例
现在我们来添加Link的测试
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
let Home = () => <div>Home</div>
let Profile = () => <div>Profile</div>
let User = () => <div>User</div>
export default class App extends Component {
render() {
return (
<div className="container">
<nav className="navbar navbar-default">
<div className="container-fluid">
<div className="navbar-header">
<div className="navbar-brand">管理系统</div>
</div>
<ul className="nav navbar-nav">
<li><Link to="/home">首页</Link> </li>
<li><Link to="/user">用户管理</Link></li>
<li><Link to="/profile">个人设置</Link></li>
</ul>
</div>
</nav>
<div className="row">
<div className="col-md-12">
{/* <Switch> */}
<Route path='/home' component={Home} />
<Route path='/user' component={User} />
<Route path='/profile' component={Profile} />
{/* </Switch> */}
</div>
</div>
</div>
)
}
}

当我们点击Link的时候,就会实现页面的跳转
代码实现
Link
Link就类似与a,当我们点击他的时候浏览器的url就会发生改变
可以直接使用a和href来实现
但是为了兼容browerRouter我们统一使用window.location.hash来更改地址
<Link to="/home">首页</Link>
根据使用用例我们知道,Link的作用有以下两个
- 应该把
Link的内容渲染出来 - 把
to的设置为最新的url
因为涉及到url的前进后退,我们有专门的一个栈来存储hash地址
当我们往这个栈存储一个值的时候,浏览器就会更改为该地址
这个栈就是hisroty
当然这里我们还是先假装父组件已经提供了这个属性
所以代码就有
import React, { Component } from 'react'
import RouterContext from './context'
export default class Link extends Component {
static contextType = RouterContext
render() {
return (
<a onClick={() => this.context.history.push(this.props.to)}>
{this.props.children}
</a >)
}
}
HashRouter.js
这里我们就要来给提供者的value添加上面需要的history
render() {
let value = {
location: this.state.location,
history: {
push: to => window.location.hash = to
}
}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
)
}
还要记得在 index.js里导出
import HashRouter from './HashRouter'
import Link from './Link'
import Route from './Route'
export { HashRouter, Link, Route }
测试用例
现在我们要来实现一个二级路由,即涉及到路由参数的跳转
我们这里就只对User添加路由
// src/components/App.js
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import User from './User'
let Home = () => <div>Home</div>
let Profile = () => <div>Profile</div>
export default class App extends Component {
render() {
return (
<div className="container">
<nav className="navbar navbar-default">
<div className="container-fluid">
<div className="navbar-header">
<div className="navbar-brand">管理系统</div>
</div>
<ul className="nav navbar-nav">
<li><Link to="/home">首页</Link> </li>
<li><Link to="/user">用户管理</Link></li>
<li><Link to="/profile">个人设置</Link></li>
</ul>
</div>
</nav>
<div className="row">
<div className="col-md-12">
<Route path='/home' component={Home} />
<Route path='/user' component={User} />
<Route path='/profile' component={Profile} />
</div>
</div>
</div>
)
}
}
这里主要就是写用户管理页面,即User

import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import UserAdd from './UserAdd';
import UserList from './UserList';
import UserDetail from './UserDetail';
export default class User extends Component {
render() {
return (
<div className="row">
<div className="col-md-2">
<ul className="nav nav-pills nav-stacked">
<li><Link to="/user/add">添加用户</Link></li>
<li><Link to="/user/list">用户列表</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>
)
}
}
这里我们我们就再也不用写Router了
所以业界的在使用Router4的时候,就是直接在最外层套一个Router
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { HashRouter as Router } from 'react-router-dom'
import 'bootstrap/dist/css/bootstrap.css'
import App from './components/App'
ReactDOM.render(
<Router>
<App />
</Router>
, document.getElementById('root'))
UserAdd
那现在我们开始写下UserAdd

<form>
<label htmlFor="">用户名</label>
<input type="text" />
<input type="submit" />
</form>

使用css美化下,现在就和上面的一样了
<form>
<div className='form-group'>
<label htmlFor="">用户名</label>
<input type="input" className='form-control' />
</div>
<div className='form-group'>
<input type="submit" className='btn btn-primary'/>
</div>
</form>
我们现在要做的就是当用户点击Submit的时候我们把输入框的内容获取
这里为了方便,我们直接使用ref直接获取
<input ref={ele => this.textInput = ele} className='form-control' />
我们这里获取的是该dom的实例,注意我们不能直接获取ele.value,因为第一次的时候ele是null
如果
ref回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
那么我们就可以通过以下代码获取到用户输入的值了
let textInput = this.textInput.value
那希望点击submit的时候就把内容存储起来
我们就在form上绑定一个事件
<form onClick={this.handleSubmit}>
// ...
</form>
handleSubmit = (event) => {
event.preventDefault()
let textInput = this.textInput.value
}
这里记得我们要阻止默认事件,不然会跳转页面
用户的数据就以id和name格式存储
handleSubmit = (event) => {
event.preventDefault()
let name = this.textInput.value
let user = { id: Date.now(), name }
}
存储的地方就使用localSorage
那我们先获取,往获取的内容添加,再重新存储进去
当然,第一次获取的时候是空的,所以如果为空的时候,我们就设置个默认值
handleSubmit = (event) => {
// ...
let usersStr = localStorage.getItem('users')
let users = usersStr ? JSON.parse(usersStr) : []
users.push(user)
localStorage.setItem('users', JSON.stringify(users))
}
添加后我们希望跳转到用户列表查看
this.props.history.push('/user/list')
最后的代码就是
import React, { Component } from 'react';
export default class UserAdd extends Component {
handleSubmit = (event) => {
event.preventDefault()
let name = this.textInput.value
let user = { id: Date.now(), name }
let usersStr = localStorage.getItem('users')
let users = usersStr ? JSON.parse(usersStr) : []
users.push(user)
localStorage.setItem('users', JSON.stringify(users))
this.props.history.push('/user/list')
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div className='form-group' onSubmit={this.handleSubmit}>
<label htmlFor="">用户名</label>
<input ref={ele => this.textInput = ele} className='form-control' />
</div>
<div className='form-group'>
<input type="submit" className='btn btn-primary' />
</div>
</form>
)
}
}
UserList
那我们首先要获取刚刚存储的数据
先在this.state添加默认值
constructor(props) {
super(props)
this.state = { users: [] }
}
然后在didmount后获取数据
componentDidMount() {
let usersStr = localStorage.getItem('users')
let users = usersStr ? JSON.parse(usersStr) : []
this.setState({ users })
}
这样获取到数据后修改this.state就会触发页面重新渲染
render() {
return (
<ul className='list-group'>
{
this.state.users.map((user) => (
<li key={user.id} className='list-group-item'>
{user.name}
</li>
))
}
</ul>
)
}

现在我们有个需求,就是点击上面的用户名,进入详情
所以我们使用Link
<li key={user.id} className='list-group-item'>
<Link to={'/user/detail/' + user.id} >{user.name}</Link>
</li>
现在页面就变成了可点击的了
比如点击后的进入的链接就是
http://localhost:3000/#/user/detail/1566283588589
UserDetail
那我们现在要写detail路由页面了
首先我们获取到已经添加好的数据
export default class UserDetail extends Component {
constructor(props) {
super(props);
this.state = { user: {} };
}
componentDidMount() {
let usersStr = localStorage.getItem('users')
let users = usersStr ? JSON.parse(usersStr) : []
console.log('<<<===users==>>>');
console.log(users);
console.log('==>>>><<<==');
}
}

我们现在要做的就是把url里的ip提取出来进行配对,然后把配对好的对象渲染出来
现在的问题就是怎么把http://localhost:3000/#/user/detail/1566283588589后面的数字提取出来
这个时候就要使用到了match
match
在match里面有一个parmas属性
我们在原本的路由是这么设置的
<Route path="/user/add" component={UserAdd} />
<Route path="/user/list" component={UserList} />
<Route path="/user/detail" component={UserDetail} />
因为我们不是严格匹配,所以
http://localhost:3000/#/user/detail/1566283588589
刚好匹配第三个路由/user/detail
如果我们把这个路由修改成
<Route path="/user/detail/:id" component={UserDetail} />
此时我们再来查看该路由渲染时有什么区别
我们在被渲染的组件里输出this.props

注意看上面的path和url,我们就能清楚的发现,url后面多出来的字符串就被提去出来存储在了match.params里面
既然知道了this.props.match.params.id是我们想要的数据
我们就可以通过find把想要的数据直接筛选出来了
let user = users.find(user => user.id === parseFloat(this.props.match.params.id))
注意了
params.id是一个字符串,而我们的user.id是一个数字,我们要进行类型转换,当然使用==就可以自动转换,但是还是不推荐使用==
我们把把数组更新到state里面
那么我们就可以直接渲染出数据了
import React, { Component } from 'react';
export default class UserDetail extends Component {
constructor(props) {
super(props);
this.state = { user: {} };
}
componentDidMount() {
let usersStr = localStorage.getItem('users')
let users = usersStr ? JSON.parse(usersStr) : []
let user = users.find(user => user.id === parseFloat(this.props.match.params.id))
this.setState({ user })
}
render() {
return (
<div>
<label>用户id: </label>{this.state.user.id}
<br></br>
<label>用户名: </label>{this.state.user.name}
</div>
)
}
}
代码实现
那在上面的代码中,我们要给上下文添加一个match.params
通过Route上的path属性地址更改为*path*="/user/detail/:id"
这样就可以通过match:params.id获取到
http://localhost:3000/#/user/detail/1566283588589中后面的数据
id = 1566283588589
很明显,我们需要使用正则对url进行提取
这里我们使用一个path-to-regexp库
path-to-regexp
Installation
npm install path-to-regexp --save
Usage
const pathToRegexp = require('path-to-regexp')
// pathToRegexp(path, keys?, options?)
// pathToRegexp.parse(path)
// pathToRegexp.compile(path)
-
path A string, array of strings, or a regular expression.
-
keys An array to ==populate with填充== keys found in the path.
/user/:id/:name上面这里的
id和name就是所谓的keys -
options
-
sensitive When
truethe regexp will be ==case sensitive(大小写敏感)==. (default:false)regxp:正则表达式 Regular expression(重定向自RegExp) -
strict When
truethe regexp allows an optional trailing delimiter to match. (default:false)如果不严格匹配的话,即
/a/和/a是匹配的 -
end When
truethe regexp will match to the end of the string. (default:true)如果
end为true的话,/a和/a/b是不匹配的 -
start When
truethe regexp will match from the beginning of the string. (default:true) -
==delimiter(定界符)== The default delimiter for ==segments(划分)==. (default:
'/') -
endsWith Optional character, or list of characters, to treat as "end" characters.
-
whitelist List of characters to consider delimiters when parsing. (default:
undefined, any character)
-
const keys = []
const regexp = pathToRegexp('/foo/:bar', keys)
// regexp = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
这里我们只讲options里面的end参数
- end: When
truethe regexp will match to the end of the string. (default:true)-
end:true
let pathToRegExp = require('path-to-regexp'); let regx = pathToRegExp('/home', [], { end: true }); console.log(regx);// /^\/home\/?$/i console.log(regx.test('/home')); //true console.log(regx.test('/home/2')); //false
-
end:false
let pathToRegExp = require('path-to-regexp'); let regx2 = pathToRegExp('/home', [], { end: false }); console.log(regx2);// /^\/home\/?(?=\/|$)/i console.log(regx2.test('/home')); //true console.log(regx2.test('/home/'));// true console.log(regx2.test('/home//')); //true console.log(regx2.test('/home/2')); //true
-
同上上面的代码我们知道
pathToRegExp返回一个正则表达式end表示是否要严格匹配后续的字符串true地址需要百分百相同faluse前面如果匹配上的话就通过
那第二个参数[]是干什么用的呢?
-
keys: An array to ==populate with填充== keys found in the path.
let pathToRegExp = require('path-to-regexp'); let keys = []; let regx3 = pathToRegExp('/user/:id', keys, { end: true }); console.log(regx3); // /^\/user\/([^\/]+?)(?:\/)?$/i console.log(keys); /* [ { name: 'id', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' } ] */
上面看的很难懂,我们把这个正则使用起来,比较好讲
let pathToRegExp = require('path-to-regexp');
let keys = [];
let regx3 = pathToRegExp('/user/:id', keys, { end: true });
let ret = '/user/1566283588589'.match(regx3)
console.log(ret);
/*
[ '/user/1566283588589',
'1566283588589',
index: 0,
input: '/user/1566283588589',
groups: undefined ]
*/
我们使用返回的正则对字符串进行过滤,于是我们发现返回数组的第二个参数是1566283588589
我们可以理解为:id是变量名,我们可以获取该变量名上具体的值
而 keys就是存储变量名的数组
我们设置多个变量名来体验
let pathToRegExp = require('path-to-regexp');
let keys = [];
let regx3 = pathToRegExp('/user/:name/:age', keys, { end: true });
console.log(keys);
/*
[ { name: 'name',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[^\\/]+?' },
{ name: 'age',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[^\\/]+?' } ]
*/
keys里面就有"变量名"name和age了
这个时候,我们再来配合正则匹配的结果
let pathToRegExp = require('path-to-regexp');
let keys = [];
let regx3 = pathToRegExp('/user/:name/:age', keys, { end: true });
// console.log(keys);
let ret = '/user/wcdaren/22'.match(regx3)
/*
[ '/user/wcdaren/22',
'wcdaren',
'22',
index: 0,
input: '/user/wcdaren/22',
groups: undefined ]
*/
我们发现返回的结果中第2和第3个参数刚好是上面keys里kye对应的value
即,{nmae : wcdaren}和{age : 22}
那我们要怎么做才能把两个数据合成上面的样子呢
let pathToRegExp = require('path-to-regexp');
let keys = [];
let regx3 = pathToRegExp('/user/:name/:age', keys, { end: true });
let [url, ...values] = '/user/wcdaren/22'.match(regx3)
keys = keys.map(key => key.name)
console.log(values); //[ 'wcdaren', '22' ]
console.log(keys); //[ 'name', 'age' ]
let params = keys.reduce((memo, key, indxe) => {
memo[key] = values[indxe]
return memo
}, {})
console.log(params); // { name: 'wcdaren', age: '22' }
那现在我们就可以开始来写我们Route了
Route
我们现在的要求是把Route的path里实现正则
我们先在constructor里面把需要的regexp和keys准备好
constructor(props) {
super(props)
let { path } = props // /user/detail/:id
this.keys = []
this.regexp = pathToRegxp(path, this.keys, { end: false })
this.keys = this.keys.map(key => key.name)
}
那我们就开始使用上面获取的正则和key
我们从上下文中提取出location.pathname然后进行正则
let pathname = this.context.location.pathname;
let keys = []
let regxp = pathToRegxp(path, keys, { end: false })
let ret = pathname.match(regxp)
如果ret不为null的话,说明匹配上了
那我们就要提取出一个key-value结构的数据作为match.params上
if (ret) {
let [url, ...values] = ret
keys = keys.map(key => key.name)
let params = keys.reduce((memo, key, index) => {
memo[key] = values[index]
return memo
}, {})
let match = {
url: pathname,
path,
params
}
let props = {
location: this.context.location,
history: this.context.history,
match
}
return <Component {...props}></Component>
}
最终的代码就是
import React, { Component } from 'react';
import Context from './context';
import pathToRegxp from 'path-to-regexp'
export default class Route extends Component {
static contextType = Context;
render() {
let { path, component: Component } = this.props;
let pathname = this.context.location.pathname;
let keys = []
let regxp = pathToRegxp(path, keys, { end: false })
let ret = pathname.match(regxp)
if (ret) {
let [url, ...values] = ret
keys = keys.map(key => key.name)
let params = keys.reduce((memo, key, index) => {
memo[key] = values[index]
return memo
}, {})
let match = {
url: pathname,
path,
params
}
let props = {
location: this.context.location,
history: this.context.history,
match
}
return <Component {...props}></Component>
}
return null;
}
}