前端路由有有 hash 路由和 history 路由兩種路由方式费彼,他們的原理是什么,又怎樣實(shí)現(xiàn)一個(gè)簡(jiǎn)單的路由監(jiān)聽呢喘鸟?
我們?cè)谑褂?Vue 或者 React 等前端渲染時(shí),通常會(huì)有 hash 路由和 history 路由兩種路由方式盈罐。
1.hash 路由:監(jiān)聽 url 中 hash 的變化,然后渲染不同的內(nèi)容闪唆,這種路由不向服務(wù)器發(fā)送請(qǐng)求盅粪,不需要服務(wù)端的支持;
2.history 路由:監(jiān)聽 url 中的路徑變化悄蕾,需要客戶端和服務(wù)端共同的支持票顾;
我們一步步實(shí)現(xiàn)這兩種路由,來深入理解下底層的實(shí)現(xiàn)原理帆调。我們主要實(shí)現(xiàn)以下幾個(gè)簡(jiǎn)單的功能:
1.監(jiān)聽路由的變化奠骄,當(dāng)路由發(fā)生變化時(shí),可以作出動(dòng)作番刊;
2.可以前進(jìn)或者后退含鳞;
3.可以配置路由;
1. hash 路由
當(dāng)頁面中的hash
發(fā)生變化時(shí)芹务,會(huì)觸發(fā)hashchange
事件蝉绷,因此我們可以監(jiān)聽這個(gè)事件,來判斷路由是否發(fā)生了變化枣抱。
window.addEventListener(
'hashchange',
function (event) {
const oldURL = event.oldURL; // 上一個(gè)URL
const newURL = event.newURL; // 當(dāng)前的URL
console.log(newURL, oldURL);
},
false
);
1.1 實(shí)現(xiàn)的過程
對(duì) oldURL 和 newURL 進(jìn)行拆分后熔吗,就能獲取到更詳細(xì)的 hash 值。我們這里從創(chuàng)建一個(gè) HashRouter 的 class 開始一步步寫起:
class HashRouter {
currentUrl = ''; // 當(dāng)前的URL
handlers = {};
getHashPath(url) {
const index = url.indexOf('#');
if (index >= 0) {
return url.slice(index + 1);
}
return '/';
}
}
事件hashchange
只會(huì)在hash
發(fā)生變化時(shí)才能觸發(fā)沃但,而第一次進(jìn)入到頁面時(shí)并不會(huì)觸發(fā)這個(gè)事件磁滚,因此我們還需要監(jiān)聽load
事件。這里要注意的是宵晚,兩個(gè)事件的event
是不一樣的:hashchange
事件中的 event
對(duì)象有 oldURL
和 newURL
兩個(gè)屬性垂攘,但 load
事件中的 event
沒有這兩個(gè)屬性,不過我們可以通過 location.hash
來獲取到當(dāng)前的 hash
路由:
class HashRouter {
currentUrl = ''; // 當(dāng)前的URL
handlers = {};
constructor() {
this.refresh = this.refresh.bind(this);
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
getHashPath(url) {
const index = url.indexOf('#');
if (index >= 0) {
return url.slice(index + 1);
}
return '/';
}
refresh(event) {
let curURL = '',
oldURL = null;
if (event.newURL) {
oldURL = this.getHashPath(event.oldURL || '');
curURL = this.getHashPath(event.newURL || '');
} else {
curURL = this.getHashPath(window.location.hash);
}
this.currentUrl = curURL;
}
}
到這里已經(jīng)可以實(shí)現(xiàn)獲取當(dāng)前的 hash
路由淤刃,但路由發(fā)生變化時(shí)晒他,我們的頁面應(yīng)該進(jìn)行切換,因此我們需要監(jiān)聽這個(gè)變化:
class HashRouter {
currentUrl = ''; // 當(dāng)前的URL
handlers = {};
// 暫時(shí)省略上面的代碼
refresh(event) {
// 當(dāng)hash路由發(fā)生變化時(shí)逸贾,則觸發(fā)change事件
this.emit('change', curURL, oldURL);
}
on(evName, listener) {
this.handlers[evName] = listener;
}
emit(evName, ...args) {
const handler = this.handlers[evName];
if (handler) {
handler(...args);
}
}
}
const router = new HashRouter();
rouer.on('change', (curUrl, lastUrl) => {
console.log('當(dāng)前的hash:', curUrl);
console.log('上一個(gè)hash:', lastUrl);
});
1.2 調(diào)用的方式
到這里陨仅,我們把基本的功能已經(jīng)完成了。來配合一個(gè)例子就更形象了:
// 先定義幾個(gè)路由
const routes = [
{
path: '/',
name: 'home',
component: <Home />,
},
{
path: '/about',
name: 'about',
component: <About />,
},
{
path: '*',
name: '404',
component: <NotFound404 />,
},
];
const router = new HashRouter();
// 監(jiān)聽change事件
router.on('change', (currentUrl, lastUrl) => {
let route = null;
// 匹配路由
for (let i = 0, len = routes.length; i < len; i++) {
const item = routes[i];
if (currentUrl === item.path) {
route = item;
break;
}
}
// 若沒有匹配到铝侵,則使用最后一個(gè)路由
if (!route) {
route = routes[routes.length - 1];
}
// 渲染當(dāng)前的組件
ReactDOM.render(route.component, document.getElementById('app'));
});
hash 路由的樣例
html代碼:
<div class="container">
<nav>
<p><a href="#/">home</a></p>
<p><a href="#/article">article</a></p>
<p><a href="#/archive#a=1">archive#a=1</a></p>
<p><a href="#/about">about</a></p>
</nav>
<div id="app"></div>
</div>
js代碼:
const { useState, useEffect, useMemo } = React;
const Home = () => {
const [count, setCount] = useState(0);
const [nowtime, setNowtime] = useState(0);
const sum = useMemo(() => ((1 + count) * count) / 2 + " ,random: " + Math.random(), [count]);
return (
<div>
<h1>Home Page</h1>
<p> count: {count}</p>
<p> sum: {sum}</p>
<p> nowtime: {nowtime}</p>
<button onClick={() => setCount(count + 1)}> add 1 </button>
<button onClick={() => setNowtime(Date.now())}> set now time </button>
</div>
);
};
const About = () => {
return (
<div>
<h1>About Page</h1>
</div>
);
};
const NotFound404 = () => {
return (
<div>
<h1>404 Page</h1>
</div>
);
};
// ReactDOM.render(<Home />, document.getElementById("app"));
const routes = [
{
path: "/",
name: "home",
component: <Home />,
},
{
path: "/about",
name: "about",
component: <About />,
},
{
path: "*",
name: "404",
component: <NotFound404 />,
},
];
const router = new HashRouter();
router.on("change", (currentUrl, lastUrl) => {
console.log(currentUrl, lastUrl);
let route = null;
for (let i = 0, len = routes.length; i < len; i++) {
const item = routes[i];
if (currentUrl === item.path) {
route = item;
break;
}
}
if (!route) {
route = routes[routes.length - 1];
}
ReactDOM.render(route.component, document.getElementById("app"));
});
2. history 路由
在 history
路由中灼伤,我們一定會(huì)使用window.history
中的方法,常見的操作有:
1.back()
:后退到上一個(gè)路由咪鲜;
2.forward()
:前進(jìn)到下一個(gè)路由狐赡,如果有的話;
3.go(number)
:進(jìn)入到任意一個(gè)路由疟丙,正數(shù)為前進(jìn)颖侄,負(fù)數(shù)為后退鸟雏;
4.pushState(obj, title, url)
:前進(jìn)到指定的 URL,不刷新頁面览祖;
5.replaceState(obj, title, url)
:用 url 替換當(dāng)前的路由孝鹊,不刷新頁面;
調(diào)用這幾種方式時(shí)展蒂,都會(huì)只是修改了當(dāng)前頁面的 URL又活,頁面的內(nèi)容沒有任何的變化。但前 3 個(gè)方法只是路由歷史記錄的前進(jìn)或者后退玄货,無法跳轉(zhuǎn)到指定的 URL皇钞;而pushState
和replaceState
可以跳轉(zhuǎn)到指定的 URL悼泌。如果有面試官問起這個(gè)問題“如何僅修改頁面的 URL松捉,而不發(fā)送請(qǐng)求”,那么答案就是這 5 種方法馆里。
如果服務(wù)端沒有新更新的 url 時(shí)隘世,一刷新瀏覽器就會(huì)報(bào)錯(cuò),因?yàn)樗⑿聻g覽器后鸠踪,是真實(shí)地向服務(wù)器發(fā)送了一個(gè) http 的網(wǎng)頁請(qǐng)求丙者。因此若要使用 history
路由,需要服務(wù)端的支持营密。
2.1 應(yīng)用的場(chǎng)景
pushState
和 replaceState
兩個(gè)方法跟 location.href
和 location.replace
兩個(gè)方法有什么區(qū)別呢械媒?應(yīng)用的場(chǎng)景有哪些呢?
1.location.href
和 location.replace
切換時(shí)要向服務(wù)器發(fā)送請(qǐng)求评汰,而 pushState
和 replace
僅修改 url
纷捞,除非主動(dòng)發(fā)起請(qǐng)求;
2.僅切換 url
而不發(fā)送請(qǐng)求的特性被去,可以在前端渲染中使用主儡,例如首頁是服務(wù)端渲染,二級(jí)頁面采用前端渲染惨缆;
3.可以添加路由切換的動(dòng)畫糜值;
4.在瀏覽器中使用類似抖音的這種場(chǎng)景時(shí),用戶滑動(dòng)切換視頻時(shí)坯墨,可以靜默修改對(duì)應(yīng)的 URL寂汇,當(dāng)用戶刷新頁面時(shí),還能停留在當(dāng)前視頻捣染。
2.2 無法監(jiān)聽路由的變化
當(dāng)我們用 history
的路由時(shí)骄瓣,必然要能監(jiān)聽到路由的變化才行。全局有個(gè)popstate
事件液斜,別看這個(gè)事件名稱中有個(gè) state 關(guān)鍵詞累贤,但pushState
和replaceState
被調(diào)用時(shí)叠穆,是不會(huì)觸發(fā)觸發(fā) popstate 事件的,只有上面列舉的前 3 個(gè)方法會(huì)觸發(fā)臼膏∨鸨唬可以點(diǎn)擊【popState 不會(huì)觸發(fā) popstate 事件】查看。
針對(duì)這種情況渗磅,我們可以使用window.dispatchEvent
添加事件:
const listener = function (type) {
var orig = history[type];
return function () {
var rv = orig.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
window.history.pushState = listener('pushState');
window.history.replaceState = listener('replaceState');
然后就可以添加對(duì)這兩個(gè)方法的監(jiān)聽了:
window.addEventListener('pushState', this.refresh, false);
window.addEventListener('replaceState', this.refresh, false);
2.3 完整的代碼
class HistoryRouter {
currentUrl = '';
handlers = {};
constructor() {
this.refresh = this.refresh.bind(this);
this.addStateListener();
window.addEventListener('load', this.refresh, false);
window.addEventListener('popstate', this.refresh, false);
window.addEventListener('pushState', this.refresh, false);
window.addEventListener('replaceState', this.refresh, false);
}
addStateListener() {
const listener = function (type) {
var orig = history[type];
return function () {
var rv = orig.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
window.history.pushState = listener('pushState');
window.history.replaceState = listener('replaceState');
}
refresh(event) {
this.currentUrl = location.pathname;
this.emit('change', location.pathname);
document.querySelector('#app span').innerHTML = location.pathname;
}
on(evName, listener) {
this.handlers[evName] = listener;
}
emit(evName, ...args) {
const handler = this.handlers[evName];
if (handler) {
handler(...args);
}
}
}
const router = new HistoryRouter();
router.on('change', function (curUrl) {
console.log(curUrl);
});