一、history簡(jiǎn)介
History 對(duì)象包含用戶(hù)(在瀏覽器窗口中)訪問(wèn)過(guò)的 URL,它是 window 對(duì)象的一部分,可通過(guò) window.history 屬性對(duì)其進(jìn)行訪問(wèn)。history對(duì)象在前端應(yīng)用中至關(guān)重要,所有單頁(yè)應(yīng)用的路由都是基于history對(duì)象。
二、導(dǎo)讀
本文會(huì)先簡(jiǎn)單介紹history對(duì)象的一些屬性,然后會(huì)重點(diǎn)介紹history對(duì)象的一些實(shí)際應(yīng)用,以此來(lái)幫助我們加深對(duì)history對(duì)象的理解。
三、屬性介紹

上圖是我在控制臺(tái)打印的history對(duì)象,下面我們簡(jiǎn)單介紹一下這些屬性。
3.1 屬性值
- length:返回瀏覽器歷史列表中的 URL 數(shù)量。
- scrollRestoration: 滾動(dòng)恢復(fù)屬性允許web應(yīng)用程序在歷史導(dǎo)航上顯式地設(shè)置默認(rèn)滾動(dòng)恢復(fù)行為。該屬性有兩個(gè)可選值,默認(rèn)為auto,將恢復(fù)用戶(hù)已滾動(dòng)到的頁(yè)面上的位置。另一個(gè)值為:manual,不還原頁(yè)上的位置,用戶(hù)必須手動(dòng)滾動(dòng)到該位置。
- state:返回一個(gè)表示歷史堆棧頂部的狀態(tài)的值,這是一種可以不必等待popstate事件而查看狀態(tài)的方式。
3.2 方法
- history.pushState(object, title, url)方法接受三個(gè)參數(shù),object 為隨著狀態(tài)保存的一個(gè)對(duì)象,title為新頁(yè)面的標(biāo)題,url為新的網(wǎng)址。
- replaceState(object, title, url) 與pushState的唯一區(qū)別在于該方法是替換掉history棧頂元素。
- history.go(x) 去到對(duì)應(yīng)的url歷史記錄。
- history.back() 相當(dāng)于瀏覽器的后退按鈕。
- history.forward() 相當(dāng)于瀏覽器的前進(jìn)按鈕。
3.3 事件
- popstate事件:popstate事件會(huì)在以下的情況觸發(fā):
同一個(gè)文檔的瀏覽歷史發(fā)生變化時(shí)觸發(fā)。調(diào)用history.pushState()和history.replaceState()方法不會(huì)觸發(fā)。而用戶(hù)點(diǎn)擊瀏覽器的前進(jìn)/后退按鈕時(shí)會(huì)觸發(fā),調(diào)用history對(duì)象的back()、forward()、go()方法時(shí),也會(huì)觸發(fā)。popstate事件的回調(diào)函數(shù)的參數(shù)為event對(duì)象,該對(duì)象的state屬性為隨狀態(tài)保存的那個(gè)對(duì)象。
3.4 理解
3.4.1問(wèn)題
介紹了history對(duì)象,我們先拋出幾個(gè)小問(wèn)題:
1.history對(duì)象可變嗎?
2.history.length既然代表瀏覽器歷史列表中的URL數(shù)量,那么這個(gè)數(shù)量可以無(wú)限多嗎?
3.location.href與history.pushState有什么區(qū)別?
4.如果我從A域名跳轉(zhuǎn)到了B域名,那么history.back()會(huì)回到哪里?
5.popstate事件的觸發(fā)條件是什么?
3.4.2 解答
下面我們來(lái)依次解答這幾個(gè)問(wèn)題,初步加深對(duì)history對(duì)象的理解。
問(wèn)題1
history對(duì)象可變嗎?
探索

我們給history賦值為空對(duì)象,然后打印一下history,可以看到history不為空對(duì)象。
結(jié)論
window.history對(duì)象是不可變的
問(wèn)題2
history.length既然代表瀏覽器歷史列表中的URL數(shù)量,那么這個(gè)數(shù)量可以無(wú)限多嗎?
探索

我們首先打印出history.length,發(fā)現(xiàn)結(jié)果為3;然后我們添加100條記錄,再次打印history.length,發(fā)現(xiàn)值為50。
結(jié)論
history.length并不會(huì)無(wú)限大
問(wèn)題3
location.href與history.pushState有什么區(qū)別?
探索
[圖片上傳中...(image.png-a52ee3-1609847856284-0)]

我們以百度h5頁(yè)面來(lái)舉例,首先我們進(jìn)入http:www.baidu.com,同時(shí)打印一下history對(duì)象,length為2。


接下來(lái)我們使用location.href = 'https://www.zhihu.com'來(lái)進(jìn)行跳轉(zhuǎn),發(fā)現(xiàn)頁(yè)面跳轉(zhuǎn)到了知乎,此時(shí)我們?cè)俅蛴∫幌耯istory,發(fā)現(xiàn)length變?yōu)榱?。


此時(shí)我們點(diǎn)擊瀏覽器的返回,再次回到百度h5頁(yè)面,打印一下history,依然為3。

此時(shí)我們使用history.pushState(null, ' ', https://www.zhihu.com'),發(fā)現(xiàn)拋出一個(gè)錯(cuò)誤,意思就是pushState是不能用來(lái)在不同域名之間跳轉(zhuǎn)的。


接下來(lái)我們使用history.pushState(null, ' ', /a'),發(fā)現(xiàn)頁(yè)面的url后面添加了一個(gè)'/a'路徑,但是觀察控制臺(tái),發(fā)現(xiàn)并沒(méi)有往服務(wù)器再發(fā)送任何請(qǐng)求。


我們?cè)偈褂靡幌耹ocation.href = '/a',發(fā)現(xiàn)瀏覽器再次發(fā)起了文檔請(qǐng)求,頁(yè)面變?yōu)榱薔ot Found
結(jié)論
1.使用location.href跳轉(zhuǎn)后頁(yè)面會(huì)發(fā)起新的文檔請(qǐng)求,而history.pushState不會(huì)。
2.location.href可以跳轉(zhuǎn)到其他域名,而history不能。
3.location.href與history都會(huì)往歷史列表中添加一條記錄。
問(wèn)題4
如果我從A域名跳轉(zhuǎn)到了B域名,那么history.back()會(huì)回到哪里?
探索

還是以百度h5頁(yè)面為例

我們使用location.href = 'https://www.zhihu.com'進(jìn)行跳轉(zhuǎn)


接著,使用history.back()方法,頁(yè)面又回到了www.baidu.com頁(yè)面
結(jié)論
從A域名跳轉(zhuǎn)到了B域名,那么調(diào)用history.back()會(huì)回到A域名
問(wèn)題5
popstate事件的觸發(fā)條件是什么?
探索

首先我們監(jiān)聽(tīng)一下popstate事件,然后我依次調(diào)用了location.href,location.hash,history.go,history.back,history.forward,history.pushState,history.replaceState方法,得出結(jié)果如下
結(jié)論
1.因?yàn)閘ocation.href是刷新式的跳轉(zhuǎn),所以這個(gè)打印信息是肯定打印不出來(lái)的,在刷新的時(shí)候這個(gè)監(jiān)聽(tīng)函數(shù)就已經(jīng)失效了,所以這里不討論location.href會(huì)不會(huì)觸發(fā)popstate事件。跟location.href類(lèi)似的還有history.go(0),因?yàn)閔istory.go(0)也會(huì)直接刷新頁(yè)面,所以這個(gè)監(jiān)聽(tīng)函數(shù)也會(huì)失效,也不會(huì)打印出信息。
2.location.hash是會(huì)觸發(fā)popstate事件的,同樣會(huì)觸發(fā)popstate的還有history.back,history.forward,history.go。
3.history.pushState,history.replaceState都不會(huì)觸發(fā)popstate事件。
四、應(yīng)用
通過(guò)以上幾個(gè)問(wèn)題,我們初步了解了history對(duì)象,下面我們來(lái)看一下它的一些實(shí)際應(yīng)用
4.1 單頁(yè)應(yīng)用
history最常見(jiàn)的使用就是搭建前端單頁(yè)應(yīng)用
使用history.pushState方法可以改變地址欄的路徑而不用刷新頁(yè)面,所以這使得我們只需要在第一次進(jìn)入頁(yè)面的時(shí)候去請(qǐng)求一次html,后續(xù)的頁(yè)面呈現(xiàn)則交由js來(lái)控制,根據(jù)不同url路徑來(lái)加載不同的js模塊。
使用history路由需要注意的是服務(wù)器需要做好處理 URL 的準(zhǔn)備,因?yàn)楫?dāng)用戶(hù)在url為'/a/b/c'的頁(yè)面進(jìn)行刷新操作,服務(wù)器很有可能會(huì)因?yàn)槠ヅ洳坏铰窂蕉祷?04狀態(tài)碼,應(yīng)當(dāng)對(duì)這樣的路徑也都返回html文件。
4.2 交互操作
問(wèn)題
另一類(lèi)比較常見(jiàn)的,就是一些交互實(shí)現(xiàn)類(lèi)。比如說(shuō)以下交互:
1.在創(chuàng)建/編輯頁(yè)面,用戶(hù)修改了表單以后,如果退出的時(shí)候,給出二次彈窗確認(rèn)。
2.在移動(dòng)端的列表頁(yè),點(diǎn)擊篩選框會(huì)彈出一個(gè)浮層,當(dāng)用戶(hù)點(diǎn)擊app的后退按鈕時(shí),把浮層關(guān)閉掉,而不是回退頁(yè)面。
3.當(dāng)前處在頁(yè)面A,點(diǎn)擊跳轉(zhuǎn)到頁(yè)面B,由頁(yè)面B內(nèi)請(qǐng)求發(fā)現(xiàn)當(dāng)前用戶(hù)無(wú)權(quán)限,于是跳轉(zhuǎn)到錯(cuò)誤頁(yè)C,如果避免用戶(hù)在C頁(yè)面點(diǎn)擊瀏覽器的回退按鈕再次回到B頁(yè)面。
解答
分析
1.交互1與交互2是同一類(lèi)問(wèn)題,原理都是點(diǎn)擊瀏覽器的前進(jìn)與后退按鈕都會(huì)觸發(fā)popstate事件,監(jiān)聽(tīng)這個(gè)popstate事件,一旦觸發(fā),便給出一個(gè)彈窗。需要注意的是,當(dāng)popstate事件觸發(fā)的時(shí)候,歷史地址記錄就已經(jīng)被回退了,我們無(wú)法阻止這個(gè)回退,所以在回退之前,我們需要使用history.pushState(null,null,document.URL)方法去主動(dòng)再添加一條當(dāng)前url的記錄,當(dāng)popstate事件觸發(fā)的時(shí)候,雖然回退了一條記錄,但是url并不會(huì)改變,也就達(dá)到了停留在當(dāng)前頁(yè)面的目的。
2.關(guān)于交互3,我們要學(xué)會(huì)使用history.replace方法,如果我們一直使用pushState或者location.href進(jìn)行跳轉(zhuǎn)的話,那么此時(shí)歷史記錄是這樣的A—B—C,但是如果我們從B到C跳轉(zhuǎn)的時(shí)候使用history.replace的話,B記錄就會(huì)被替換為C記錄,那么歷史記錄就會(huì)變?yōu)锳—C,此時(shí)從C頁(yè)面點(diǎn)擊返回按鈕就可以直接返回A頁(yè)面。
實(shí)例
下面我給出一個(gè)點(diǎn)擊瀏覽器的后按鈕后彈窗的效果,供大家參考。
還是以百度h5頁(yè)面舉例,在'/a'頁(yè)面,我點(diǎn)擊返回的時(shí)候,會(huì)彈出禁止返回的彈窗。

具體代碼如下,可在控制臺(tái)使用
history.pushState(null, null, '/a')
window.addEventListener('popstate', () => {
alert('禁止返回')
})
history.pushState(null, null, document.URL)
4.3 各種路由框架的基礎(chǔ)
路由框架通常都有三種模式:browserHistory,hashHistory,memoryHistory,其中browserHistory的實(shí)現(xiàn)就是依賴(lài)于window.history對(duì)象,下面我們先來(lái)想兩個(gè)問(wèn)題,然后接著來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的前端單頁(yè)路由。
問(wèn)題
1.用window.history.pushState和路由框架的pushState有什么區(qū)別?
2.既然使用history.pushState無(wú)法觸發(fā)popstate事件,那么路由框架又是如何在pushState的時(shí)候加載不同組件的呢?
3.為什么使用pushState跳轉(zhuǎn)以后,history對(duì)象的state里都有一個(gè)屬性key?
解答
下面咱們來(lái)分析一下這幾個(gè)問(wèn)題。
實(shí)驗(yàn)

首先我們掘金的首頁(yè),點(diǎn)擊前端板塊,發(fā)現(xiàn)在進(jìn)入'/frontend'路徑時(shí),并沒(méi)有發(fā)送html請(qǐng)求,說(shuō)明這是一個(gè)單頁(yè)應(yīng)用,下面我們?cè)俜祷厥醉?yè),使用history.pushState(null, null, '/frontend')來(lái)進(jìn)入前端板塊,看看會(huì)發(fā)生什么。

可以看到,此時(shí)url已經(jīng)變了,但是頁(yè)面并沒(méi)有渲染出前端模塊。

我們順勢(shì)來(lái)看一看vue-router的源碼,我們可以看到它調(diào)用了一個(gè)pushState函數(shù),我們來(lái)看看這個(gè)函數(shù)

并沒(méi)有看出什么特別的地方,這兒的pushState就是調(diào)用了history.pushState函數(shù)。不過(guò)從這里我們看出了問(wèn)題3的答案,vue-router在使用push函數(shù)的時(shí)候調(diào)用了history.pushState方法,而這里在使用history.pushState函數(shù)時(shí)往里面加了一個(gè)key。

我們可以看到這個(gè)key的值就是一個(gè)時(shí)間,有什么特殊含義嗎?后來(lái)查閱官方文檔,得出了這樣的解釋?zhuān)?br>
當(dāng)一個(gè) history 通過(guò)應(yīng)用程序的 push 或 replace 跳轉(zhuǎn)時(shí),它可以在新的 location 中存儲(chǔ) “l(fā)ocation state” 而不顯示在 URL 中,這就像是在一個(gè) HTML 中 post 的表單數(shù)據(jù)。 在 DOM API 中,這些 hash history 通過(guò) window.location.hash = newHash 很簡(jiǎn)單地被用于跳轉(zhuǎn),且不用存儲(chǔ)它們的location state。但我們想全部的 history 都能夠使用location state,因此我們要為每一個(gè) location 創(chuàng)建一個(gè)唯一的 key,并把它們的狀態(tài)存儲(chǔ)在 session storage 中。當(dāng)訪客點(diǎn)擊“后退”和“前進(jìn)”時(shí),我們就會(huì)有一個(gè)機(jī)制去恢復(fù)這些 location state。我們?cè)倩氐街暗膯?wèn)題一與問(wèn)題二,既然這個(gè)pushState沒(méi)有什么特別的,我們?cè)賮?lái)看一看這個(gè)transitionTo函數(shù)。

我發(fā)現(xiàn)了這段代碼,這里調(diào)用了該路由的回調(diào)函數(shù)。眾所周知,我們注冊(cè)一個(gè)路由一般是采用這種形式
router.route('/111', state => { contentDOM.innerHTML = '111';});這里就是執(zhí)行了state => { contentDOM.innerHTML = '111'; }這個(gè)回調(diào)函數(shù),所以問(wèn)題就清楚了,路由框架的pushState不僅調(diào)用了history.pushState方法,還調(diào)用了該路由對(duì)應(yīng)的回調(diào)函數(shù)來(lái)渲染了對(duì)應(yīng)的組件。
結(jié)論
所以我們得出結(jié)論,路由框架的pushState與history.pushState是不一樣的,路由框架的pushState不僅調(diào)用了history.pushState改變了url,更重要的是它還多了一步操作,即根據(jù)這個(gè)url銷(xiāo)毀了舊組件,渲染了新組件;至于state里面的key值,則是為了兼容hashHistory。
前端路由demo
下面我們來(lái)實(shí)現(xiàn)一個(gè)前端路由的demo,現(xiàn)在已經(jīng)有一個(gè)html,我們需要為它寫(xiě)一個(gè)Router,實(shí)現(xiàn)如下效果:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>前端路由實(shí)現(xiàn)</title>
<style>
.link {
color: #999;
cursor: pointer;
}
.link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<ul>
<li><a class="link" data-href="/A">A</a></li>
<li><a class="link" data-href="/B">B</a></li>
<li><a class="link" data-href="/C">C</a></li>
<li><a class="link" data-href="/D">D</a></li>
</ul>
<div id="wrapper"></div>
<script>
// 創(chuàng)建實(shí)例
const router = new Router();
const contentDOM = document.querySelector('#wrapper');
// 注冊(cè)路由
router.route('/A', state => {
contentDOM.innerHTML = 'A';
});
router.route('/B', state => {
contentDOM.innerHTML = 'B';
});
router.route('/C', state => {
contentDOM.innerHTML = 'C';
});
router.route('/D', state => {
contentDOM.innerHTML = 'D';
});
</script>
</body>
</html>
簡(jiǎn)單分析一下:
1.首先發(fā)布訂閱模式肯定少不了,注冊(cè)路由的時(shí)候,需要將每個(gè)路由所對(duì)應(yīng)的回調(diào)函數(shù)存儲(chǔ)起來(lái),在路由變化的時(shí)候執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
2.只監(jiān)聽(tīng)popSate是不夠的,頁(yè)面初始化的時(shí)候,以及pushState的時(shí)候,都需要執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)去主動(dòng)更新一下組件。
3.還有一個(gè)問(wèn)題,就是需要阻止這幾個(gè)a標(biāo)簽的默認(rèn)事件。
經(jīng)過(guò)以上對(duì)history的理解,這個(gè)簡(jiǎn)單的Router已經(jīng)不難實(shí)現(xiàn)了,下面直接給出完整代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>前端路由實(shí)現(xiàn)</title>
<style>
.link {
color: #999;
cursor: pointer;
}
.link:hover {
text-decoration: underline;
}
</style>
<script>
const noop = () => undefined;
class Router {
constructor() {
this.init();
}
// 初始化
init() {
this.routes = {};
this.doListen();
this.makeLink();
}
// 監(jiān)聽(tīng)
doListen() {
window.addEventListener('DOMContentLoaded', this.listenEventInstance.bind(this));
window.addEventListener('popstate', this.listenEventInstance.bind(this));
}
// 監(jiān)聽(tīng)事件后,觸發(fā)路由的回調(diào)
listenEventInstance() {
this.callbackCenter(window.location.pathname);
};
// 注冊(cè)路由,將回調(diào)函數(shù)存儲(chǔ)下來(lái)
route(pathname, callback = noop) {
this.routes[pathname] = callback;
}
// 回調(diào)
callbackCenter(pathname) {
if (!this.routes[pathname]) {
return;
}
const {state} = window.history;
this.routes[pathname](state);
}
// 綁定 a 標(biāo)簽,阻止默認(rèn)行為
makeLink() {
document.addEventListener('click', e => {
const {target} = e;
const {nodeName, dataset: {href}} = target;
if (!(nodeName === 'A') || !href) {
return;
}
e.preventDefault();
window.history.pushState(null, '', href);
this.callbackCenter(href);
});
}
}
</script>
</head>
<body>
<ul>
<li><a class="link" data-href="/A">A</a></li>
<li><a class="link" data-href="/B">B</a></li>
<li><a class="link" data-href="/C">C</a></li>
<li><a class="link" data-href="/D">D</a></li>
</ul>
<div id="wrapper"></div>
<script>
// 創(chuàng)建實(shí)例
const router = new Router();
const contentDOM = document.querySelector('#wrapper');
// 注冊(cè)路由
router.route('/A', state => {
contentDOM.innerHTML = 'A';
});
router.route('/B', state => {
contentDOM.innerHTML = 'B';
});
router.route('/C', state => {
contentDOM.innerHTML = 'C';
});
router.route('/D', state => {
contentDOM.innerHTML = 'D';
});
</script>
</body>
</html>
五、總結(jié)
本文首先介紹了history對(duì)象的各個(gè)屬性,然后介紹了它的一些應(yīng)用,希望本文能在實(shí)際工作中對(duì)大家有所幫助。在前端路由這塊兒除了window.history以外,其他知識(shí)點(diǎn)以及相關(guān)應(yīng)用還有很多。對(duì)于location對(duì)象、搭建多頁(yè)應(yīng)用等其他知識(shí),大家感興趣的話可以去深入探究。
六、參考
- jqhtml.com: 單頁(yè)應(yīng)用的部署方案
- 掘金: 性能 & 集成 —— History API
- react-router: react-router文檔
- vue: vue源碼
- MDN: history對(duì)象