隨著 ajax 的流行,異步數據請求體驗極具提升,用戶得以在不刷新瀏覽器的情況下進行頁面交互,而異步交互體驗的更高級版本就是 SPA —— 單頁應用。
單頁應用不僅僅是在頁面交互時無刷新,連頁面跳轉都是無刷新的,為了實現單頁應用,就有了前端路由。
常用的兩種模式
類似于服務端路由解析對應的 url 路徑,返回對應的頁面/資源的方式,前端路由實現起來其實也很簡單,就是匹配不同的 url 路徑,進行解析,然后動態的渲染出區域 html 內容。
這樣自然 url 每次變化的時候,都會造成頁面的刷新。
那么在改變 url 的情況下,如何保證頁面的不刷新?
hash 模式
在 2014 年之前,大家是通過 hash 來實現路由,url hash 就是類似于:
https://www.xxx.com/#/login
這種 # 后面 hash 值的變化,并不會導致瀏覽器向服務器發出請求,瀏覽器不發出請求,也就不會刷新頁面。
為什么改變 hash 不刷新頁面?——URL的井號‘#’
‘#’ 代表網頁中的一個位置,它后面的字符,就是該位置的標識符,它只對瀏覽器有用,服務器不識別,因此 HTTP 請求不會包含 #
(想要請求 url 包含 # ,可使用 encodeURIComponent()
進行部分轉義)
改變 hash ,只會讓瀏覽器滾動到相應位置,不會重載網頁
每次 hash 值的變化,會觸發 hashchange 事件,通過window.onhashchange
監聽該事件我們就可以檢測變化的 hash 值來做相應的頁面操作。
簡易實現
接下來我們用最簡單的代碼實現 hash 模式,僅為了解其思想(你可以直接復制到一個 html 上并通過靜態服務器如 http-server 查看):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Hash 路由</title>
</head>
<body>
<ul>
<li><a href="#red">紅色背景</a></li>
<li><a href="#green">綠色背景</a></li>
<li><a href="#grey">灰色背景</a></li>
</ul>
<script>
function watchHash() {
const hash = window.location.hash.slice(1) || '/';
switch (hash) {
case "red":
document.body.style.background = "red";
break;
case "green":
document.body.style.background = "green";
break;
case "grey":
document.body.style.background = "grey";
break;
}
}
window.addEventListener("hashchange", watchHash, false);
window.addEventListener("load", watchHash, false);
</script>
</body>
</html>
如何實現最基礎的前進后退?
這里我們簡單實現一下后退功能,前進思路類似:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Hash 路由</title>
</head>
<body>
<ul>
<li><a href="#red">紅色背景</a></li>
<li><a href="#green">綠色背景</a></li>
<li><a href="#grey">灰色背景</a></li>
</ul>
<button id="back" disabled="true">后退</button>
<script>
var isGoBack = false;
const history = [];
function watchHash() {
const hash = window.location.hash.slice(1) || "/";
// 防止后退時也記錄 hash
if (!isGoBack && window.location.hash) {
history.push(window.location.hash);
}
back.disabled = history.length > 0 ? false : true;
console.log(history);
switch (hash) {
case "red":
document.body.style.background = "red";
break;
case "green":
document.body.style.background = "green";
break;
case "grey":
document.body.style.background = "grey";
break;
default:
document.body.style.background = "#fff";
}
isGoBack = false;
}
back.onclick = goBack;
function goBack() {
isGoBack = true;
if (history.length > 0) {
history.pop(1);
window.location.hash = history[history.length - 1] || "";
} else {
back.disabled = true;
}
}
window.addEventListener("load", watchHash, false);
window.addEventListener("hashchange", watchHash, false);
</script>
</body>
</html>
思路就是通過一個數組記錄每次 hashchange 事件的 hash 值,點擊后退時取出上一次 hash 值覆蓋當前頁面的 hash。
需要注意的是需要區別當前 hash 是后退生成(后退時的 hash 變化不應記錄)的還是跳轉生成,避免重復記錄。
history 模式
可以看到,在早期 hash 模式雖然可以實現前端路由,但其后退前進操作就十分麻煩。
2014 年后,HTML5 引入了 History API,讓我們能夠快速訪問頁面歷史。
其中 history.pushState() 和 history.replaceState() 方法,它們分別可以添加和修改歷史記錄條目,通過這兩個 API 可以改變 url 地址而無須重新加載頁面。
同時還有 popstate 事件:
通過window.onpopstate
可以監聽在瀏覽器點擊后退、前進按鈕(或者在 JavaScript 中調用 history.back()、history.forward()、history.go() 方法) 觸發的 popstate 事件。
通過這些就能用另一種方式來實現前端路由了,但原理都是跟 hash 實現相同的。
用 history 實現上面 hash 代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>History 路由</title>
</head>
<body>
<ul id="ul">
<li><a href="/red">紅色背景</a></li>
<li><a href="/green">綠色背景</a></li>
<li><a href="/grey">灰色背景</a></li>
</ul>
<script>
const path = window.location.pathname;
history.replaceState({ path: path }, null, path);
ul.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault();
const path = e.target.getAttribute("href");
history.pushState({ path: path }, null, path);
watchHistory(path);
}
});
function watchHistory() {
const path = window.location.pathname;
switch (path) {
case "/red":
document.body.style.background = "red";
break;
case "/green":
document.body.style.background = "green";
break;
case "/grey":
document.body.style.background = "grey";
break;
default:
document.body.style.background = "#fff";
}
}
window.addEventListener("popstate", watchHistory, false);
</script>
</body>
</html>
用了 HTML5 的實現,單頁路由的 url 就不會多出一個 #,變得更加美觀。
但因為沒有 # 號,所以當用戶刷新頁面之類的操作時,瀏覽器還是會給服務器發送請求。
為了避免出現這種情況,history 模式需要服務器的支持,把所有路由都重定向到根頁面。
如何監聽 pushState 和 replaceState 的變化
經過理論及實踐我們知道 replaceState(),pushState() 兩個 API 不會觸發 popstate 監聽事件。
我們可以生成全新的 window 監聽事件監聽其變化:
function addListen(type) {
const source = history[type];
return function () {
const event = new Event(type);
event.arguments = arguments;
window.dispatchEvent(event);
return source.apply(this, arguments);
};
}
history.pushState = addListen("pushState");
history.replaceState = addListen("replaceState");
window.addEventListener("replaceState", (e) => {
console.log("我監聽了 replaceState");
});
window.addEventListener("pushState", (e) => {
console.log("我監聽了 pushState");
});
兩種模式對比
- 無 # 的 history 模式更自然
- history 模式需要 IE9 以上,相對于 hash 模式的 IE8 兼容性差
- history 模式需服務器端配合,反過來說 hash 模式不支持服務端渲染
結語
以上就是前端路由的 hash 和 history 兩種模式的主要原理及實現思路了,如果你覺得不錯,別忘了點個贊??!
本文參考: