参考一文了解 history 和 react-router 的实现原理react-router 版本
前端路由
如何实现前端路由
实现前端路由,需要解决两个核心问题
- 如何改变 URL 却不引起页面刷新?
- 如何监测 URL 变化?
hash 模式
- hash模式下,改变url中的
hash
部分不会引起页面的刷新
- 通过 hashchange 事件监听 URL 的改变。
- 改变 URL 的方式 : 浏览器导航栏的
前进后退
、通过<a>
标签、通过window.location
基于 hash 实现
js
<ul>
<!-- 定义路由 -->
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
<script>
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onHashChange)
// 监听路由变化
window.addEventListener('hashchange', onHashChange)
// 路由视图
let routerView = document.querySelector('#routeView')
// 路由变化时,根据路由渲染对应 UI
function onHashChange() {
switch (location.hash) {
case '#/home':
routerView.innerHTML = 'Home'
return
case '#/about':
routerView.innerHTML = 'About'
return
default:
routerView.innerHTML = 'Not Found'; return;
}
}
</script>
history 模式
- history 提供了
pushState
和replaceState
两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新 - 通过
popstate
事件监听 URL 的改变。 - 只有浏览器导航栏的
前进后退
改变 URL 时会触发popstate
事件
History.pushState() 与 History.replaceState()
object
:是一个对象,通过 pushState 方法可以将该对象内容传递到新页面中。在title
:不用url
:新的网址,必须与当前页面处在同一个域。
js
history.pushState({ foo: 'bar' }, '', '2.html');
console.log(history.state) // {foo: "bar"}
popstate 每当 history 对象出现变化时,就会触发 popstate 事件。
js
window.addEventListener('popstate', function(e) {
//e.state 相当于 history.state
console.log('state: ' + JSON.stringify(e.state));
console.log(history.state);
});
基于 history 实现
因为 history 模式下,<a>
标签和pushState
/replaceState
不会触发popstate
方法,我们需要对<a>
的跳转和pushState
/replaceState
做特殊处理。
- 对
<a>
作点击事件,禁用默认行为,调用pushState
方法并手动触发popstate
的监听事件 - 对
pushState
/replaceState
可以重写 history 的方法并通过派发事件能够监听对应事件
重写pushState
js
var _wr = function (type) {
var orig = history[type];
return function () {
var e = new Event(type);
e.arguments = arguments;
var rv = orig.apply(this, arguments);
window.dispatchEvent(e);
return rv;
};
};
history.pushState = _wr("pushState");
//每次调用`history.pushState`时,都会触发一个名为`pushState`的自定义事件。
window.addEventListener("pushState", function (e) {
onPopState();
});
重写a
js
var linkList = document.querySelectorAll("a[href]");
linkList.forEach((el) =>
el.addEventListener("click", function (e) {
e.preventDefault();
history.pushState(null, "", el.getAttribute("href"));
})
);
}
js
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
<script>
var _wr = function (type) {
var orig = history[type];
return function () {
var e = new Event(type);
e.arguments = arguments;
var rv = orig.apply(this, arguments);
window.dispatchEvent(e);
return rv;
};
};
history.pushState = _wr("pushState");
</script>
</head>
<body>
<ul>
<!-- 定义路由 -->
<li><a href="/home">home</a></li>
<li><a href="/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
<script>
var routeView = null;
function onLoad() {
routerView = document.querySelector("#routeView");
onPopState();
// 拦截 <a> 标签点击事件默认行为
// 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
var linkList = document.querySelectorAll("a[href]");
linkList.forEach((el) =>
el.addEventListener("click", function (e) {
e.preventDefault();
history.pushState(null, "", el.getAttribute("href"));
})
);
}
// Use it like this:
window.addEventListener("pushState", function (e) {
onPopState();
});
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件,处理默认hash
window.addEventListener("DOMContentLoaded", onLoad);
// 监听路由变化
window.addEventListener("popstate", onPopState);
// 路由变化时,根据路由渲染对应 UI
function onPopState() {
switch (location.pathname) {
case "/home":
routerView.innerHTML = "This is Home";
return;
case "/about":
routerView.innerHTML = "This is About";
return;
case "/list":
routerView.innerHTML = "This is List";
return;
default:
routerView.innerHTML = "Not Found";
return;
}
}
</script>
</body>
</html>