理解瀏覽器歷史記錄(2)- hashchange、pushState

發表於2016-10-21

本文也是一篇基礎文章。繼上文之後,本打算去研究pushState,偶然在一些資訊中發現了錨點變化對瀏覽器的歷史記錄也會影響,同時錨點的變化跟pushState也有一些關聯。所以就花了點時間,把這兩個東西儘量都琢磨清楚。本文記錄相關的一些要點及研究過程。

1. hashchange

這個部分的內容也已經補充到上文的最後了,這裡只是細化一下。總的結論是:如果一個網頁只是錨點,也就是location.hash發生變化,也會導致歷史記錄棧的變化;且變化相關的所有特性,都與上文描述的整個頁面變化的特性相同。常見的改變網頁錨點的方式有:

1)直接更改瀏覽器地址,在最後面增加或改變#hash;
2)通過改變location.href或location.hash的值;
3)通過觸發點選帶錨點的連結;
4)瀏覽器前進後退可能導致hash的變化,前提是兩個網頁地址中的hash值不同。

假如我們還用上文的demo來測試,並按照以下步驟操作的話:
開啟新選項卡;輸入demo1.html;在位址列後面加#1;將位址列#1改成#2;將位址列#2改成#3;將位址列#3改成#1。
那麼歷史記錄棧的儲存狀態就應該類似下面這個形式:

459873-20161014155747562-1970766655

由於錨點變化也會在歷史記錄棧新增新的記錄,所以history.length也會在錨點變化之後改變。每當錨點發生變化的時候,主流瀏覽器還會觸發window物件的onhashchange事件,在這個事件回撥裡面,我們通過事件物件和location能夠拿到很有用三個引數:

event.oldURL返回錨點變化前的完整瀏覽器地址;
event.newURL返回錨點變化後的完整瀏覽器地址;
location.hash返回錨點變化後頁面地址中的錨點值。

藉助於這三個資訊,可以在hashchange回撥內加一些控制器的邏輯,來實現單頁程式開發裡面關鍵的路由功能。現簡單實現舉例如下:

本程式碼demo可通過以下地址訪問測試:http://liuyunzhuge.github.io/blog/pushState/demo1.html。這個demo中,瀏覽器前進後退,頁面重新整理,連結跳轉,都能保證內容正確顯示。當然這只是一個極為簡單的舉例,真正的SPA的路由功能遠比此複雜,下一步我會花時間研究一個較為流行的路由實現,到時再寫文來總結單頁路由的實現思路。

window.onhashchange的mdn參考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onhashchange

以上是我瞭解到hashchange的絕大部分用得著的內容,下面要介紹的pushState,還會有一點跟它相關的東西。在SPA的路由實現中,hashchange與pushState是搭配在一起使用的,所以在真正瞭解路由實現前,把這2個東西的基礎知識瞭解透徹也是非常有必要的。

2 . pushState

有了之前對歷史記錄棧的認識,再來了解pushState就會比較容易。pushState相關的內容包含三個東西:2個api和一個事件。2個api分別是history.pushState和history.replaceState,1個事件是指window.onpopstate事件。pushState提供給我們的是一種在不改變網頁內容的前提下,操作瀏覽器歷史記錄的能力。

下面詳細看看這2個api和1個事件的內容:

1)history.pushState(stateObj,title,url)

這個方法用來在瀏覽器歷史記錄棧中當前指標後面壓入一條新的條目,然後將當前指標移到這條最新的條目;如果在壓入新條目的時候,當前指標的後面還有舊的條目,在壓入新的之後也會被廢棄掉。整體特性其實跟上一篇部落格介紹的,在同一個視窗開啟另外一個頁面對歷史記錄棧的作用完全相似,只不過history.pushState僅僅是新增新的條目,並且啟用它,然後改變瀏覽器的地址,但是不會改變網頁內容,它也不會去驗證這個新條目對應的網頁是否存在。

這個api有三個引數,第二個引數目前瀏覽器都是忽略它的,在使用的時候一般傳入空字串即可;第三個引數對應的是新條目的地址,如果沒有,預設就是當前文件的地址;第一個引數是一個object物件,它會與新條目繫結在一起,可以用來儲存一些簡單的資料,不過不能存太多,firefox對它的限制是640K,這個物件可以通過onpopstate事件物件的state屬性來訪問。

為了驗證前面這部分的理論,可以通過這個demo:http://liuyunzhuge.github.io/blog/pushState/demo2.html,按以下步驟做一些操作測試:
開啟新選項卡;輸入該demo地址;點選demo3的連結;點選demo4的連結;點選demo4裡的返回;點選demo3裡的返回;點選pushState(‘foo’)的按鈕;點選pushState(‘bar’)的按鈕。

瀏覽器歷史記錄棧的變化過程應該是下面這個狀態:
459873-20161020215910826-1874159974

2)history.replaceState(stateObj,title,url)

這個api和history.pushState的用法完全一致,只不過它不會在歷史記錄棧中增加新的條目,只會影響當前條目,比如如果傳遞了stateObj,就會更新當前條目關聯的狀態物件;如果傳遞了url,就會替換當前條目的頁面地址和更改瀏覽器位址列的地址。有一種非常常見的場景,如果利用replaceState,可以優化它的實現方式。

網頁中搜尋列表是比較常見的功能:

459873-20161020215912513-294630814-2

有2種常見的方式來實現這樣的功能:
一是將查詢條件區封裝好,列表展示區封裝好,當查詢條件改變的時候,利用ajax,觸發列表的查詢;但是這種方式有個不好的體驗問題就是,查詢條件更改後,如果重新整理頁面,查詢條件不能恢復重新整理前的狀態;所以就有了第二種方式;
二是在查詢條件更改的時候,不用ajax更換列表,而是更新url引數,重新重新整理頁面,然後在後端或在前端將查詢條件的狀態根據url裡面的引數初始化好再展示。

目前電商都是第二種方式多,一來比較簡單,二來相容性也好。如果不考慮相容IE9以前的瀏覽器,利用replaceState可以優化第一種做法:就是在查詢條件更改的時候,除了用ajax查詢資料,同時用replaceState更新頁面的url,把條件封裝到url引數中;當使用者重新整理頁面時,根據url裡面的條件引數做查詢條件的初始化,這一步跟第二個方案的做法一致。

history.pushState和history.replaceState還有一個共同的特點就是都不會觸發hashchange,你可以下面這個demo來測試:http://liuyunzhuge.github.io/blog/pushState/demo5.html,以新選項卡開啟這個demo,不管先點選什麼按鈕,頁面上都不會看到有任何的列印資訊,儘管我在程式碼中是有新增window.onhashchange回撥的:
459873-20161020230751810-1380925141
但是當我直接在位址列後面新增一個#3的時候,頁面上就會看到onhashchange回撥列印的資訊了:
459873-20161020230752513-534969621

3) window.onpopstate事件

這個事件觸發的時機比較有特點:
一、history.pushState和history.replaceState都不會觸發這個事件
二、僅在瀏覽器前進後退操作、history.go/back/forward呼叫、hashchange的時候觸發
你可以下面這個demo來驗證:http://liuyunzhuge.github.io/blog/pushState/demo6.html,這個demo裡我新增了onpopstate回撥,嘗試列印一些資訊,如果按以下幾組步驟測試:
a. 開啟新選項卡,輸入demo地址,點選pushState的按鈕,再點選瀏覽器的後退按鈕,再點選瀏覽器前進按鈕;
b. 開啟新選項卡,輸入demo地址,點選pushState的按鈕,點選replaceState的按鈕,再點選瀏覽器的後退按鈕,再點選瀏覽器前進按鈕;
c. 開啟新選項卡,輸入demo地址,點選#yes的連結,再點選瀏覽器的後退按鈕,再點選瀏覽器前進按鈕;
d. 開啟新選項卡,輸入demo地址,點選location.hash = ‘#no’的連結,再點選瀏覽器的後退按鈕,再點選瀏覽器前進按鈕。
最後會得到的結果如下:
a. 點選pushState的按鈕不會有列印資訊,點選後退按鈕後會有列印資訊,再點選前進按鈕會有列印資訊;
b. 點選pushState&replaceState的按鈕不會有列印資訊,點選後退按鈕後會有列印資訊,再點選前進按鈕會有列印資訊;
c&d. 點選連結,點選後退按鈕,點選前進按鈕都會有列印資訊。
雖然測試的場景不多,但是也夠我們去判斷前面那兩點結論的正確性了。

比較有意思的是,history.pushState會增加歷史記錄的條目,但是不會觸發hashchange和popstate;hashchange也可以增加歷史記錄的條目,但是它卻可以觸發popstate。[疑惑]

前面介紹說到pushState和replaceState的第一個引數stateObj,會與第三個引數對應的歷史條目繫結在一塊,當popstate事件觸發的時候,意味著有新的歷史記錄條目被啟用,在popstate的事件物件裡面,有一個state屬性,會返回這個啟用條目關聯的stateObj物件的拷貝。一個歷史記錄條目只有當它是被pushState建立的,或者用replaceState改過的,才可能有關聯的stateObj物件,所以當某些非這2種條件的歷史記錄條目被啟用的時候,可能拿到的stateObj就是null,正如你在demo6裡面看到的列印資訊顯示的那樣。

stateObj是會被持久化的硬碟上進行儲存的,至少firefox是這麼說的,我猜只要歷史記錄不銷燬,它關聯的stateObj就會一直存在。所以假如某一個網頁在使用者最後一次操作後,有關聯某個stateObj,那麼當使用者再次開啟這個網頁的時候,它的stateObj也是可以被訪問的。如果要直接訪問當前網頁對應條目的stateObj,可以通過history.state屬性來訪問。

firfox,chrome在頁面首次開啟時都不會觸發popstate事件,但是safari會。。。

popstate事件作用範圍僅在於一個document裡面,由於pushState和hashchange都不會改變網頁的內容也就是document,所以這樣的網頁裡面才能有效使用popstate。假如我們輸入一個網頁,並且在它裡面新增了popstate回撥;然後通過連結跳轉的方式轉到另外一個網頁;再點選後退按鈕回到第一個網頁。這樣的情況,第一個網頁裡面的popstate回撥,除了有可能因為頁面初始化被觸發外,瀏覽器的後退前進是不會觸發它的,因為這種方式改變了視窗的document。

以上就是pushState的相關內容。現在主流的SPA路由主要是靠pushState,它比hashchange的優勢,我認為最大的一點就是url的友好性,因為它比hashchange看起來更像是常規的跳轉操作,可是體驗上又跟hashchange一樣,不會給使用者造成瀏覽器發生了重新整理的感覺;而且從url的規劃層面來說,pushState的url跟原來的url形式都是根據具體場景而定的,hashchange可能就得用同一個url加不同的hash的形式了,這種形式對於系統設計跟seo來說也是不合理的。缺點就是pushState的相容性沒有hashchange那麼靠前。要是在移動端,這個自然就不成問題了。

pushState參考資料:

https://developer.mozilla.org/zh-CN/docs/DOM/Manipulating_the_browser_history

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/onpopstate

相關文章