一、回顾一下官方react-router-dom的使用
要想自己实现一个react-router-dom,就必须先了解一下react-router-dom的基本使用,react-router-dom与vue-router有点不同,react-router-dom采用的是去中心化路由,所以其不需要写一个单独的router.js来集中配置和管理路由,如:
①引入react-router-dom模块,react-router-dom中包含了两种路由,即HashRouter和BrowserRouter,其中HashRouter采用的是hash值的变化来切换路由,BrowserRouter采用的是history api来切换路由。
// 引入HashRouter
import {HashRouter as Router} from "react-router-dom";
// 或者引入BrowserRouter,二者引入其中的一个即可
import {BrowserRouter as Router} from "react-router-dom";
② 用<Router>组件包裹<App />
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root')
);
③ 在需要使用路由的地方,引入<Route>组件即可。
import {Route} from "react-router-dom"
class App extends Component {
render() {
<Route path="/" component={Home} exact></Route>
<Route path="/about" component={About}></Route>
<Route path="/user/:name/:age" component={User}></Route>
}
}
④ 光引入<Route>组件只能根据浏览器地址栏渲染相应的组件,我们还需要引入<Link>组件才能显示相应的路由切换链接(a标签),如:
import { Link } from "react-router-dom";
<!--使用Link组件会渲染成a标签,可以进行路由的切换-->
<ul className="nav">
<li><Link to="/">Home</Link></li>
<li><Link to= {{ pathname: "/about", query: {name: "lihb"}}}>About</Link></li>
<li><Link to="/user/lihb/28">User</Link></li>
</ul>
⑤ react路由中路径匹配的时候,如果有多个<Route>匹配到,那么匹配到的<Route>都会被渲染出来,我们可以用<Switch>组件将一组<Route>包裹起来,这样就只会渲染第一个匹配到的<Route>组件,如:
import { Switch, Route } from "react-router-dom";
<!--使用Switch组件避免重复渲染-->
<Switch>
<Route path="/" component={Home} exact></Route>
<Route path="/about" component={About}></Route>
<Route path="/user/:name/:age" component={User}></Route>
</Switch>
⑥ 为了避免所有<Route>都无法匹配的时候,显示空白页面,我们可以引入一个<Redirect>组件将其重定向到某个页面,如:
import { Redirect, Switch, Route } from "react-router-dom";
<Switch>
<Route path="/" component={Home} exact></Route>
<Route path="/about" component={About}></Route>
<Route path="/user/:name/:age" component={User}></Route>
<Redirect to="/"></Redirect><!--重定向到首页-->
</Switch>
二、react-router-dom原理
react-router-dom利用了Context API,通过上下文对象将当前路由信息对象注入到<Router>组件中,所以<Router>组件中render()渲染的内容就是Context API提供的Provider组件,然后接收<Router>组件中的当前路由信息对象。
这样<Router>组件下的所有组件都能通过上下文拿到当前路由信息对象,即其中的<Switch>、<Route>、<Link>、<Redirect>等组件都可以拿到当前路由信息对象,然后通过改变当前路由信息来实现动态切换<Route>组件的渲染。
三、开始实现自己的react-router-dom
① 创建上下文
在src目录下新建一个 src/react-router-dom/context.js文件, 其作用就是引入React并创建上下文对象,然后导出即可,如:
import React from "react";
export default React.createContext();
② 实现HashRouter,将当前路由注入到上下文中
这里先以HashRouter为例,在react-router-dom目录下新建一个HashRouter.js文件,这里我们引入上下文对象,然后通过其提供的Provider组件,将<Router>组件中的当前路由对象注入到上下文中,并将<Router>内的子组件包含进来:
import React, {Component} from "react";
import Context from "./context"; // 引入上下文对象
export default class HashRouter extends Component {
render() {
const currentRoute = { // 当前路由对象
}
return (
// 使用上下文对象提供的Provider组件将当前路由信息对象注入上下文中,以便其Route等子组件能够获取到当前路由信息
<Context.Provider value={currentRoute}>
{this.props.children}
</Context.Provider>
);
}
}
③ 填充当前路由信息
在react中,当前路由信息对象包含了location、match、history三个对象,为了方便操作,我们将location和match对象定义到state中,因为这两个对象中的数据是需要动态修改的,如:
// 在HashRouter中新增构造函数,并且将location和match定义到state对象中
constructor(props) {
super(props);
this.state = { // 为了方便更改当前路由信息,将location和match放到state中
location: {
pathname: window.location.hash.slice(1) || "/", // 获取浏览器地址栏中的hash值,如果不存在则默认为"/"
query: undefined
},
match: {
params: undefined
}
}
}
// 填充当前路由信息,新增了一个history对象
const currentRoute = { // 当前路由对象
location: this.state.location, // location直接从state中获取
match: this.state.match, // match直接从state中获取
history: { // 新增一个history对象用于实现当前路由的切换
push: (to) => {
if (typeof to === "object") { // 如果to是一个对象
let {pathname, query} = to; // 取出pathname和query
window.location.hash = pathname; // 更新浏览器hash值,触发浏览器hashchange事件
this.state.location.query = query; // 更新query
} else { // 修改浏览器地址栏的hash值
window.location.hash = to; // 更新浏览器hash值
}
}
}
}
这里给当前路由信息中添加了一个history对象,主要用于实现路由的切换,以便其子组件(如: Link)可以通过上下文拿到当前路由信息进行路由的切换,如:
// 直接传递路径进行路由的切换
this.context.history.push("/about");
// 传入一个对象,可以在切换路由的时候传递参数,比如query
this.context.history.push({ pathname: "/about", query: {name: "lihb"}})
如果传递的是路径字符,那么直接更新浏览器地址的hash值;如果传递是一个对象,那么从对象中取出要跳转的路径和传递的参数,同样更新浏览器地址栏的hash值,并将传递的参数保存到location的query中,react中还有search、state等参数,这里只以query为例。
④ 监听浏览器地址栏hash值的变化
上面调用push()方法后,仅仅改变了浏览器地址栏的hash值,也就是说仅仅路径发生了变化,而页面其实并未跟着发生变化,react中要想页面发生变化,必须调用setState()方法,我们可以监听浏览器地址栏hash值的变化,当hash值变化的时候,更新当前路由信息,并调用setState()函数触发<Router>组件渲染,其子组件也会跟着重新渲染,从而渲染出当前路由对应的组件,如:
componentDidMount() {
window.addEventListener("hashchange", () => { // 监听浏览器地址栏hash值变化
this.setState({ // 当hash值变化后更新当前路由信息, HashRouter组件内的子组件Route将会重新渲染
location: {
...this.state.location,
pathname: window.location.hash.slice(1) // 更新pathname
}
});
});
}
因为currentRoute当前路由对象定义在了render函数内,所以调用setState()后,render()函数重新执行,currentRoute更新,<Route>根据当前路由信息渲染出当前路由对应的组件。
⑤ 创建Route路由子组件
在react-router-dom目录下新建一个Route.js,Route组件要做的事就是从上下文对象中取出当前路由信息,即当前路由的路径,然后取出<Route>组件中配置的path,然后比较这两个值是否相同,如果相同,则渲染出<Route>组件上配置的component,如:
import React, {Component} from "react";
import context from "./context";
/**
* 从上下文对象中获取到当前路由信息,将其中配置的path与当前路由的pathname相同的Route渲染出来
*/
export default class Route extends Component {
static contextType = context;
render() {
const currentRoutePath = this.context.location.pathname; // 从上下文中获取到当前路由的路径
const {path, component:Component} = this.props; // 获取给Route组件传递的props属性
const props = {
...this.context
}
if (currentRoutePath === path) {
return (
<Component {...props}></Component>
);
}
return null; // 必须有返回值
}
}
⑥ 让Route可以处理带冒号参数的路径
此时已经可以匹配一些写死的路径了,但是如果是带冒号参数的路径则无法匹配,比如 /user/:name/:age 则无法被/user匹配到,路由中处理路径的匹配,需要用到path-to-regexp模块,path-to-regexp模块提供了pathToRegexp, match两个方法,match方法可以拿到匹配的params,pathToRegexp可以检测路径是否匹配成功,如:
const matchFun = match("/user/:name/:age");
console.log(matchFun("/user/lihb/28"));
// 输出结果为
{path:"/user/lihb/28", index:0, params:{name:"lihb",age:"28"}}
const regexp = pathToRegexp("/user/:name/:age", []);
console.log(regexp.test("/user/lihb/28")); // true
console.log(regexp.test("/user1/lihb/28")); // false
console.log(regexp.test("/user/lihb/28/abc")); // false
支持exact
exact就是精确匹配,react中不加exact是非精确匹配,而加上exact则是精确匹配,只有路径完全一致才能匹配成功,可以通过pathToRegexp方法的第三个参数进行控制,如:
const regexp = pathToRegexp("/user/", [], {end: false}); // 非精确匹配,即能匹配比/user更长的路径
console.log(regexp.test("/user")); // true
console.log(regexp.test("/user/lihb/28")); // true
const regexp = pathToRegexp("/user", [], {end: true}); // 精确匹配
console.log(regexp.test("/user")); // true
console.log(regexp.test("/user/lihb/28")); // false
完整的Route.js为
import React, {Component} from "react";
import context from "./context";
import {pathToRegexp, match} from "path-to-regexp";
/**
* 从上下文对象中获取到当前路由信息,将其中配置的path与当前路由的pathname相同的Route渲染出来
*/
export default class Route extends Component {
static contextType = context;
render() {
const currentRoutePath = this.context.location.pathname; // 从上下文中获取到当前路由的路径
const {path, component:Component, exact=false} = this.props; // 获取给Route组件传递的props属性
const paramsRegexp = match(path, {end: exact}); // 生成获取params的正则表达式
const matchResult = paramsRegexp(currentRoutePath);
console.log(`matchResult is ${JSON.stringify(matchResult)}`);
this.context.match.params = matchResult.params; // 将匹配的参数保存到上下文的match对象中
const props = {
...this.context
}
const pathRegexp = pathToRegexp(path, [], {end: exact}); // 生成匹配路径的正则表达式
if (pathRegexp.test(currentRoutePath)) {
return (
<Component {...props}></Component>// 将当前路由对象信息当做props传递给组件
);
}
return null; // 必须有返回值
}
}
⑦ 实现Link组件
在react-router-dom目录下新建一个Link.js,Link组件非常简单,其实就是在Link组件上配置了一个to属性,表示要去的目标路由,然后渲染成一个标签,当点击<Link>标签的时候跳转到目标路由,所以需要给标签添加一个onClick事件,通过上下文拿到当前路由信息对象,通过其push()方法跳转到目标路由即可,如:
import React, {Component} from "react";
import context from "./context";
export default class Link extends Component {
static contextType = context;
render() {
let {to} = this.props; // 从Link组件上获取到配置的to路径
// 点击Link的时候调用当前路由的history的push方法进行跳转即可
return (
<a onClick={() => {this.context.history.push(to)}}>{this.props.children}</a>
);
}
}
⑧ 实现Redirect组件
在react-router-dom目录下新建一个Redirect.js,<Redirect>组件要做的事情就是,在所有的<Route>组件都未能匹配的情况下,直接重定向到<Redirect>组件配置的路径,所有<Redirect>组件非常简单,即无限制,直接通过当前路由对象的push方法重定向即可。
import React, {Component} from "react";
import context from "./context";
export default class Redirect extends Component {
static contextType = context;
render() {
// 无限制,直接重定向跳转到指定的路径
this.context.history.push(this.props.to);
return null;
}
}
⑨ 实现Switch组件
在react-router-dom目录下新建一个Switch.js,<Switch>组件的作用就是,<Switch>的子组件中,如果有多个<Route>组件匹配成功,那么只渲染第一个匹配成功的<Route>,所以,我们需要遍历<Switch>的子组件,如果匹配成功则立即返回作为整个<Switch>组件的渲染结果。
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() {
const children = Array.isArray(this.props.children) ? this.props.children : [this.props.children];
for (let i = 0; i < children.length; i++) { // 遍历Switch组件下的所有子组件
const child = children[i];
const {path="/", exact=false} = child.props;
const {pathname} = this.context.location;
const regexp = pathToRegexp(path, [], {end: exact});
if (regexp.test(pathname)) { // 如果匹配成功则立即返回作为<Switch>组件的渲染结果
return child;
}
}
return null;
}
}
⑩ 同样的原理实现BrowserRouter
在react-router-dom目录下新建一个BrowserRouter.js,BrowserRouter和HashRouter原理一样,只不过BrowserRouter监听的是popstate事件。
import React, {Component} from "react";
import Context from "./context"; // 引入上下文对象
export default class BrowserRouter extends Component {
constructor(props) {
super(props);
this.state = {
location: {
pathname: window.location.pathname || "/",
query: undefined,
},
match: {
params: undefined
}
}
}
componentDidMount() {
window.addEventListener("popstate", () => { // 监听浏览器前进后退按钮
this.setState({ // 触发页面更新
location: {
pathname: window.location.pathname // 路径变化后更新当前路由对应的pathname
}
});
});
}
render() {
const currentRoute = { // 构造一个当前路由对象,保存当前路由信息,包括location、match、history
location: this.state.location, // location直接从state中获取
match: this.state.match, // match直接从state中获取
history: { // 新增一个history对象用于实现当前路由的切换
push: (to) => {
if (typeof to === "object") { // 如果to是一个对象
let {pathname, query} = to; // 取出pathname和query
this.state.location.query = query; // 更新query
this.state.location.pathname = pathname;// 更新当前路由对应的pathname
window.history.pushState({}, {}, pathname); // 添加一条history
} else { // 如果to是一个路径字符串
this.state.location.pathname = to; // 更新当前路由对应的pathname
window.history.pushState({}, {}, to); // 添加一条history
}
this.setState({}); // 触发页面更新
}
}
}
return (
// 使用上下文提供的Provider组件将当前路由信息对象注入上下文中,以便其Route子组件能够获取到当前路由信息
<Context.Provider value={currentRoute}>
{this.props.children}
</Context.Provider>
);
}
}
⑪ 最后再将这些组件导出即可
在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 BrowserRouter from "./BrowserRouter"
export {
HashRouter,
Route,
Link,
Redirect,
Switch,
BrowserRouter
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。