React的router实现原理
react的router分为hash 模式 和 history 模式
hash模式
hash 是 url 中 hash(#) 及后面的部分,常用锚点在页面内做导航,改变 url 中的 hash 部分不会引起页面的刷新 通过 hashchange 事件监听 URL 的改变。改变 URL 的方式只有以下几种:通过浏览器导航栏的前进后退、通过 a 标签、通过window.location,这几种方式都会触发hashchange事件
history模式
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新 通过 popchange 事件监听 URL 的改变。需要注意只在通过浏览器导航栏的前进后退改变 URL 时会触发popstate事件,通过a标签和pushState/replaceState不会触发popstate方法。但我们可以拦截a标签的点击事件和pushState/replaceState的调用来检测 URL 变化,也是可以达到监听 URL 的变化,相对hashchange显得略微复杂
JS实现前端路由
基于hash实现
BrowserRouter使用 history 库提供的createBrowserHistory创建的history对象改变路由状态和监听路由变化。
- 监听路由变化的listen方法以及对应的清理监听unlisten方法
- 改变路由的push方法
js
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件,处理默认hash
window.addEventListener('DOMContentLoaded', onLoad);
// 监听路由变化
window.addEventListener('hashchange', onHashChange);
// 路由变化时,根据路由渲染对应 UI
function onHashChange() {
switch (location.hash) {
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;
}
}
基于 history 实现
BrowserRouter使用 history 库提供的createBrowserHistory创建的history对象改变路由状态和监听路由变化。
- 监听路由变化的listen方法以及对应的清理监听unlisten方法
- 改变路由的push方法
html
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
<script>
function onClick() {
window.history.pushState({}, null, '/list');
}
// Add this:
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>
<div onclick="onClick()">点击</div>
<!-- 渲染路由对应的 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) {
console.info('addEventListener-pushState-1');
onPopState();
});
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件,处理默认hash
window.addEventListener('DOMContentLoaded', onLoad);
// 监听路由变化
window.addEventListener('popstate', () => {
console.info('addEventListener-popstate-2');
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>
History 实现
js
/**
* 创建和管理listeners的方法
*/
export const EventEmitter = () => {
const events = [];
return {
subscribe(fn) {
events.push(fn);
return function () {
events = events.filter(handler => handler !== fn);
};
},
emit(arg) {
events.forEach(fn => fn && fn(arg));
},
};
};
const createBrowserHistory = () => {
const EventBus = EventEmitter();
// 初始化location
let location = {
pathname: '/',
};
/**
* 路由变化时的回调
*/
const handlePop = function () {
const currentLocation = {
pathname: window.location.pathname,
};
EventBus.emit(currentLocation); // 路由变化时执行回调
};
/**
* 定义history.push方法
*/
const push = path => {
const { history } = window;
// 为了保持state栈的一致性
history.pushState(null, '', path);
// 由于push并不触发popstate,我们需要手动调用回调函数
location = { pathname: path };
EventBus.emit(location);
};
const listen = listener => EventBus.subscribe(listener);
// 处理浏览器的前进后退
window.addEventListener('popstate', handlePop);
// 返回history
const history = {
location,
listen,
push,
};
return history;
};