在 JavaScript 當道的今日,使用 JS 進行網頁的操作如 Ajax、頁面切換、彈出視窗等等越來越常見,甚至進一步出現了 React JS、Vue JS 等可以幫助你開發 Single Page Application (SPA) 的 JS Framework;在使用 JS 進行網頁操作時最怕的問題莫過於網址的變化與歷史紀錄,例如透過 jQuery 進行頁面切換後,因為是 JS 的行為所以網址不會變化,而且當使用者按下瀏覽器左上角的上一頁時,會直接回到別的頁面,因為在歷史紀錄裡面不會紀錄 JS 的操作(事實上也無法紀錄),要解決這一系列問題,可以透過 HTML5 提供的 history.pushState、history.replaceState 以及 onpopstate 事件來解決。
如果使用 React JS 或 Vue JS 這類框架,通常會有搭配的套件例如 vue-router 可以幫你處理 SPA 的惱人網址問題,背後原理其實也是透過 JS 修改 history 的方式處理。但有的時候你沒有好用的工具人來幫忙,就只能自己從頭刻起。 當然,我相信一定有很多 JS Plugin 可以解決這篇中所提到的問題,但更重要的是了解背後的原理,才能加以應用。
pushState、replaceState、onpopstate 這組工具可以解決哪些問題呢? 這邊試列部分我有想到的,但也別忘記他們的核心概念是修改瀏覽器歷史紀錄與網址,藉著這個核心概念可以想到更多用法,而不僅僅是拘泥在我們所看到的可能性。
- 透過 JS 對頁面進行操作後,讓網址隨之改變,方便使用者複製分享或儲存;但也別忘記,在頁面 onload 時要記得分析網址內容,讓程式會跑到正確的地方,正所謂一進一出都要兼顧(顯示網址、透過該網址回到相同內容)。
- 單純覺得網址太醜,想要偽造一個好看的網址,但要記得當使用者複製這個網址給朋友時,對方能否順利看到內容?
- 頁面有自己的上一步、下一步按鈕,透過 JS 動態載入內容,但如果使用者點擊瀏覽器上一頁時會出錯,因為自始自終都在同一頁裡面,只是透過 JS 修改內容。
這組 API 目前在 Chrome、Firefox、Safari 等等都穩定支援,使用上無需太擔心,但手機瀏覽器可能需要再進行測試。 (Ref MDN)
Feature | Chrome | Firefox | IE | Opera | Safari |
---|---|---|---|---|---|
replaceState, pushState | 5 | 4.0 (2.0) | 10 | 11.50 | 5.0 |
history.state | 18 | 4.0 (2.0) | 10 | 11.50 | 6.0 |
核心原理,這邊十分重要,這系列 API 核心概念是把 history.state 視為 Stack,你可以透過 pushState 把新的紀錄放進去,或是當使用者點擊上一頁(下一頁)時,會觸發 onpopstate 事件,並將Stack 的最後一個項目 pop 出來給你,你接收事件後再用 JS 判斷該切換到哪一頁或做什麼行為。 因為 history.state 是Stack 的緣故,如果你在 onpopstate 裡面又進行 pushState ,就可能會遇到靈異現象,原因和這篇 【Python 千萬不要在迴圈裡面刪除陣列元素】 類似;此外在 onpopstate 裡面是沒有方向性的,你無法直接得知使用者是按下一頁還是上一頁,但也不重要,謹記這是Stack 即可。
pushState
當使用者按下一步,JS 動態載入內容並跳頁,這就很適合使用 pushState,顧名思義就是把新的狀態放進 window.history 裡。
history.pushState(state, title, url)
state 參數可以是任何能夠被序列化的東西,他會被儲存在 history 裡面,當使用者按咧覽器的「上一頁」時會被 Pop 出來給你,所以透過這個參數可以讓未來的你知道「上一頁」是要切換到哪頁。
舉例來說,現在的網址是 http://example.com,當我執行下面語法後:
history.pushState({step: 1}, "第一步", "/step/1")
網址會變成 http://exmple.com/step/1,網頁標題則變成「第一步」(Firefox 似乎未實作此參數),當你按下瀏覽器的上一頁時,你會透過 onpopstate 事件獲得 {step: 1} 這個物件。
replaceState
參數和 pushState 完全一樣,差別在於 history.replaceState 不會對Stack 放東西進去,而是修改目前的狀態,通俗話就是改網址啦,舉例來說 JS 開了一個對話框,你希望這個行為被反映到網址上,但又不希望他變成歷史紀錄,那就可以使用 replaceState 啦!
onpopstate
這是一個事件,當使用者點擊瀏覽器的上一頁或下一頁按鈕時會觸發該事件,並將Stack 的最後一個項目 pop 出來給你,透過該項目(也就是你透過 pushState 推進去的 state)可以知道上一頁做了什麼,接著再透過 JS 把剛剛的內容載入回來。
window.onpopstate = function(event) { console.log(event.state); }
方向性
如果你希望判斷使用者是按了上一頁或下一頁按鈕,例如載入動畫的方向不同,預設是無法取得的,但你可以透過全域變數的設定,來計算是往哪個方向移動,請看範例:
var current_step = 1; window.onpopstate = function(event) { if(current_step - event.state.step > 0) { console.log("上一頁"); }else{ console.log("下一頁"); } } function show_step1( ) { current_step = 1; history.pushState({step: 1}, "第一步", "/step/1"); } function show_step2( ) { current_step = 2; history.pushState({step: 2}, "第二步", "/step/2"); }
注意事項
- 不要用上一頁的方式去思考,瀏覽器並不會告訴你上一頁是什麼,它是告訴你最後一個被推進去的 state 是什麼,你要解讀這個 state 自己去載入相應的內容(元素),如果你什麼都沒做,那即使使用者按「上一頁」也不會發生任合事情。
- 當 onpopstate 發生時,瀏覽器會自動幫你修改網址,無需自行呼叫 replaceState。
- 千萬不要在 onpopstate 裡面呼叫 pushState,除非你很清楚問題是如何發生的,以及你自己在做什麼。
- 這一系列 API 只在相同頁面才能運作,換言之當使用者的上一頁是透過瀏覽器載入(例如超連結)時,當他按「上一頁」,JS 不會收到任何通知。
- 因為瀏覽器的 BFCache,捲軸位置可能和預期不同。
- 不要光顧著顯示好看的網址,也要思考如果這個網址被複製分享,別的人能否「使用」該網址檢視到預期的內容。
感謝分享,正在找這資料,非常有幫助!
sorry,補充一下,onpopstate的範例中console打錯字了:D
謝謝提醒,剛剛已經修正上去囉!!
感謝分享,學到很多,世界有你真好。