前言
俗话说,想了解一个东西,不如动手去实现一个东西,当你能实现以后,并能讲给别人听以后,那么才算你真正的理解
我讲东西一般都按照what,why,how来讲的
首先提出第一个问题
通过本篇文章我们可以学到什么?
createContext的使用- history API的使用
什么是单页面路由?
路由的本质是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新(无须刷新,这个重点)。
第二个问题也来了
单页面路由原理是什么?

实现URL与UI界面的同步。其中在react-router中,URL对应Location对象,而UI是由react components来决定的,这样就转变成location与components之间的同步问题
那么如何实现呢?
reactRouter实现路由的两种模式
-
hash模式(对应HashRouter)
www.test.com/#/name就是Hash URL,当#后面的哈希值发生变化时,不会向服务器请求数据,可以通过hashchange事件来监听到URL的变化,从而进行跳转页面。 -
browser模式(对应BrowserRouter)
History模式使用 HTML5 提供的 history API(pushState、replaceState 和 popstate 事件)来保持 UI 和 URL 的同步
两种实现方式的区别是什么?
-
兼容性问题
一看到browser模式是html5新推出的功能,就可以知道区别主要是
兼容性问题,browser模式主要是在高版本浏览器使用的,hash模式主要是在老版本浏览器使用的, -
写法的不同
BrowserRouter 创建的 URL 格式:xxx.com/path
HashRouter 创建的 URL 格式:xxx.com/#/path
history中新增的方法
pushState(state,title,url)
该方法的作用是 在历史记录中新增一条记录,改变浏览器地址栏的url,但是,不刷新页面。
pushState对象接受三个参数,
- state:一个与添加的记录相关联的状态对象,主要用于popstate事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填null。
- title:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。
- url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
举个例子,假设当前网址是hello.com/1.html,使用pushState()方法在浏览记录中添加一个新纪录
var stateObj={foo:'bar'}
history.pushState(starteObj,'','2.html')
添加新纪录后,浏览器的地址栏立刻显示`hello.com/2.html,但不会跳转到2.html,也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。
总之,pushState()方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,使用该方法后,就可以使用history.state属性读出状态对象
var stateObj={foo:'bar'}
history.pushState(starteObj,'','2.html')
history.state //=> {foo:"bar"}
注意:如果pushState的URL参数设置了一个新的hash值,并不会触发hashchange事件。
replaceState(state,title,url)
replaceState方法的作用是替换当前的历史记录,其他的都与pushState()方法一模一样。
假定当前网页是example.com/example.html。
history.pushState({page: 1}, 'title 1', '?page=1')
// URL 显示为 http://example.com/example.html?page=1
history.pushState({page: 2}, 'title 2', '?page=2');
// URL 显示为 http://example.com/example.html?page=2
history.replaceState({page: 3}, 'title 3', '?page=3');
// URL 显示为 http://example.com/example.html?page=3
history.back()
// URL 显示为 http://example.com/example.html?page=1
history.back()
// URL 显示为 http://example.com/example.html
history.go(2)
// URL 显示为 http://example.com/example.html?page=3
popstate事件
popstate事件是window对象上的事件,配合pushState()和replaceState()方法使用。当同一个文档(可以理解为同一个网页,不能跳转,跳转了就不是同一个网页了)的浏览历史出现变化时,就会触发popstate事件。
上面我们说过,调用pushState()或者replaceState()方法都会改变当前的历史记录,仅仅调用pushState()方法或replaceState()方法 ,并不会触发该事件,另外一个条件是用户必须点击浏览器的倒退按钮或者前进按钮,或者使用js调用history.back()或者history.forward()等方法。
所以,记住popstate事件触发的条件
1. 处在同一个文档(同一个html页面)
2. 文档的浏览历史(即history对象)发生改变
只要符合这两个条件,popstate事件就会触发
具体例子
//index.html
<head>
<script>
window.onpopstate=function(){
alert('location '+document.location+',state '+jsON.stringify(event.state))
}
</script>
</head>
<body>
<!--第二步 -->
<button onclick="window.history.back()">后退</button>
<button onclick="window.history.forward()">前进</button>
<!--第一步 -->
<button onclick="window.history.pushState(null,'','1.html')">pushState</button>
</body>
先点击pushState按钮,在点击后退按钮,就会触发popstate事件
再来一个例子
//index.html
<head>
<script>
window.onpopstate=function(){
alert('location '+document.location+',state '+JSON.stringify(event.state))
}
</script>
</head>
<body>
<a href="#one">#one</a>
</body>
直接点击a标签,也可以触发popstate事件
browserRouter如何使用
我们先来实现react-router的browser模式,因为这是大家最常用的
我们先来看BrowserRouter是如何使用的(一定要把抽象的事情具体化)
首先我们用create-react-app创建一个项目
接着安装react-router-dom
从react-router-dom中取出BrowserRouter,Route等
import {BrowserRouter, Route, Routes} from 'react-router-dom';
import User from './User';
import Name from './Name';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" Component={User}></Route>
<Route path="/name" Component={Name}></Route>
</Routes>
</BrowserRouter>
)
}
export default App
其余的没用的删掉
在创立两个组件
这是user组件,可以跳转到name组件
export default function User(props) {
const onGoToName = () => {
props.history.push("/name");
};
return (
<div>
user<button onClick={onGoToName}>跳转</button>
</div>
);
}
这是name组件
export default function Name() {
return <div>name</div>;
}
我们可以测试一下
点击进行跳转到name组件
点击浏览器的回退按钮,进行回退到user组件
用法就是这样的演示完毕
接下来我们分析一下

流程图就如上
其实就主要两个步骤,监听浏览器,或者跳转操作,更新UI
接下来我们手写一个mini-react-router-dom
步骤如下
- 监听路由变化
popstate,设置path - 页面刷新
- 页面跳转
pushstate,提供push方法
实现步骤
这个是BrowserRouter.js
import { useEffect, useState, createContext } from "react";
/**
* @description: 1.监听路由变化(popState) 2.页面刷新 3.页面跳转(pushState) -->4.改变path变量
* @param {*}
* @return {*}
*/
export const RouterContext = createContext();
export const HistoryContext = createContext();
export default function BrowserRouter(props) {
// 页面刷新
const [path, setPath] = useState(() => {
const pathname = window.location.pathname;
return pathname || "/";
});
// 改变path
const handlePopState = () => {
// ui同步
const { pathname } = window.location;
setPath(pathname);
};
// 监听popstate
useEffect(() => {
// 路由操作
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
const push = (path) => {
// 路由操作
window.history.pushState({ path }, null, path);
// ui同步
setPath(path);
};
return (
<RouterContext.Provider value={path}>
<HistoryContext.Provider
value={{
push,
}}
>
{props.children}
</HistoryContext.Provider>
</RouterContext.Provider>
);
}
这个是Router
import { useContext } from "react";
import { RouterContext } from "./BrowserRouter";
export default function Route(props) {
const { path: RoutePath, component: RouteComponent } = props;
const path = useContext(RouterContext);
return path === RoutePath ? <RouteComponent /> : null;
}
接下来是如何使用 index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route } from "./mini-router-dom";
import "./index.css";
import User from "./view/User";
import Name from "./view/Name";
ReactDOM.render(
<BrowserRouter>
<Route path="/" component={User} />
<Route path="/name" component={Name} />
</BrowserRouter>,
document.getElementById("root")
);
User页面中如何跳转到Name页面
import { useContext } from "react";
import { HistoryContext } from "../mini-router-dom/BrowserRouter";
export default function User() {
const { push } = useContext(HistoryContext);
const onGoToName = () => {
push("/name");
};
return (
<div>
user<button onClick={onGoToName}>跳转</button>
</div>
);
}
完美的实现了我们开头的功能
总结
所以还是要掌握学习的方法,尤其是这种学习源码的,可以先从本质->原理->实现步骤图->学习涉及到的api和知识点->看源码->写源码这样一步一步来,最后还是希望大家能从本篇文章中学到更多的知识,谢谢大家