前言
事情是這樣的,測試反饋了一個問題,小程式中使用webview
開啟了某個h5頁,出現了在蘋果手機中進入h5頁面後無法返回的現象,排查的過程學到一個新知識,叫 歷史修剪(history pruning)
,很多人應該都沒聽說過,這裏跟大家分享一下。
排查思路
這個 h5 頁是其他部門的團隊在維護,我們只是在小程式中嵌入一下,所以沒辦法直接看程式碼。
1. 給出初步思路,協調溝通
出現這個問題,我首先想到的就是這個頁面應該是存在重定向的,webview
開啟 https://aaa
時,該地址會立即重定向到 https://bbb
,使用者點選返回回到 https://aaa
時,頁面又自動跳轉到 https://bbb
,如此循環往復,導致終端使用者無法退出H5,當然如果連續快速點返回有機率在重定向之前退出頁面的,但是如果是這樣的話應該所有手機表現都是一致的,而現狀是隻有蘋果手機能夠復現。
和對方團隊交涉後,對方表示他們的 h5 在app裡和瀏覽器裡都沒這個問題,讓我們自行排查小程式的相容性問題,雖然明顯不是小程式端的問題,但是拿出有理有據的證據來比互相扯皮強,那就開查唄。
2. 嘗試復現
上面的邏輯是沒問題的,他們確實使用 replace
重定向了,我首先找了另外幾個存在重定向的h5,在小程式中的 webview
開啟,結果都沒有復現,無論蘋果還是安卓都可以正常返回。
於是我就寫了個極簡的網頁,初始化就執行重定向到另一個網頁,這樣方便排查和除錯。
3. 從頁面棧入手
直接寫一個 replace 肯定是無法復現了,我們都知道瀏覽器有個叫頁面棧(Page Stack)的東西,它是瀏覽器用來管理使用者在網頁瀏覽過程中歷史記錄的一種數據結構,具有後進先出的特性。每次使用者訪問一個新頁面,瀏覽器會將這個頁面的資訊推入棧頂,當用戶點選“後退”按鈕時,瀏覽器會從棧頂彈出當前頁面,並顯示下一個頁面。
那麼先在 chrome 開啟測試頁面,注意要在新Tab開啟,確保頁面棧無其他歷史記錄,然後開啟控制檯,輸入 history
,可以看到 length 為2,即正常情況下,從新Tab頁正常開啟一個頁面後,當前頁面棧的歷史記錄應為2,由於我們使用了 replace 重定向,所以長度仍然是2。
歷史記錄爲2意味著你可以返回一層,即返回你的瀏覽器的初始頁
那麼我們的測試頁面沒啥問題,符合預期,然後再看一下對方有問題的 h5 頁,一樣的操作,嘿!您猜怎麼着,歷史記錄長度為3!
看樣子找到問題的根源了,多了一個歷史記錄,理論上正常認知下點一次就是應該無法返回到初始頁的,因為畢竟多了一個記錄,但是為啥有的能返回有的返回不了呢,包括點選瀏覽器的返回也是可以直接返回到初始頁的,這說明是不是有歷史記錄被瀏覽器忽略了,我們來驗證一下。
4. 加一個歷史記錄來驗證
在測試頁面中使用 window.history.pushState(null, '', window.location.href);
新增一個歷史記錄,這樣在完成重定向後就也是3個歷史記錄了,然後在小程式中試一下。
OK,不出所料,在 IOS 中復現了,出現了返回重定向返回重定向...,在安卓中則沒問題,包括安卓手機中的瀏覽器(三星瀏覽器),然後我又試了一下在釘釘中開啟這個h5,也出現了返回重定向返回重定向...的現象,看來其實也不是平臺問題,就是某些瀏覽器核心的機制,具體什麼機制還不得而知。
5. 用 history.back() 驗證
之前我們懷疑由於瀏覽器的某些策略忽略了某些頁面棧中的歷史記錄,現在我們用 history.back()
來驗證一下,因為 back 是程式設計方式,肯定會嚴格遵循頁面棧順序的嘛,它不會忽略歷史記錄。
在瀏覽器的控制檯中輸入 history.back()
,嘿,您猜又怎麼着?果然不出所料,使用 history.back()
在chrome 中完美復現鬼打牆,但是在chrome中手動點返回就不行。
OK,到這裏,我們真正的找到了癥結所在,就是瀏覽器的返回按鈕包含某些策略會忽略掉一些歷史記錄,接下來只需要找出這個策略就好了。
所以,這個問題實際上是和小程式沒有任何關係的
6. 問 AI
這種沒有明確目標的用Google肯定不太好搜,問AI肯定效率更高。
問 claude
實際上同樣的問題我問了好幾個AI,包括 Gemini 、必應copilot、ChatGPT,最後只有claude提出了 history pruning 的概念,不愧是 claude
claude
提出了 history pruning
的概念,看著挺靠譜,不知道真假,問別的 AI 驗證一下
看著依舊很靠譜,由於 history pruning
的策略,瀏覽器會在使用者回退時忽略某些瞬態或者它認為不重要(重複等)的歷史記錄。
畢竟是 AI,還是得抱著將信將疑的心態,此時我們就有明確的搜尋目標了,使用 Perplexity
搜尋一下
Perplexity 是一個聚合搜尋工具,它能自動幫你整理整合搜索結果,不用自己一個一個點進去詳情檢視
從Perplexity整合的結果來看,瀏覽器確實有這種策略來避免 “回退陷阱”,我們點進去來源頁看看
大概意思就是,如果有淘氣的開發人員新增了N多無意義的歷史記錄,那麼使用者就需要點N多下才能返回到初始頁,這樣使用者體驗極差,這個策略一定程度上防止了這種情況,即允許瀏覽器自動使用 history.go(-N)
來最佳化。
大家感興趣可以自行找找其他官方說法
延伸:關於挽留彈窗
因為 pushState
方法只會改變頁面棧的歷史記錄,並不會進行實際的跳轉操作,所以我們經常會用新增頁面棧歷史記錄的方式來實現“挽留彈窗”的功能
React Vue 等框架的路由庫就是基於pushState、onpopstate等來實現的,細節很多,大致最終目標就是在不重新整理瀏覽器的情況下維護應用的路由以及更新或區域性更新頁面資料。
當用戶點選返回時回退一個頁面同時展示彈窗,因為新增了一個和當前頁面地址相同的記錄,所以使用者是無感的,對於使用者來說相當於沒有回退,類似下面這樣實現:
window.history.pushState(null, '', window.location.href); const historyReturnCb = () => { // 觸發挽回彈窗 window.removeEventListener('popstate', historyReturnCb); }; window.addEventListener('popstate', historyReturnCb);
那麼我們思考一下,這樣新增的記錄會被上面所說的最佳化策略作為無用記錄給最佳化掉嗎?從而導致挽留彈窗無法生效?
答案是肯定的,只要你用的瀏覽器啟用了最佳化策略,你的挽留彈窗必定不會生效,那這樣肯定不行啊,這個屬於是正當訴求啊,不能 "誤殺" 啊。
對於這種情況,瀏覽器也是有對應的解決策略的,那就是當用戶點選了你的頁面時,即使用者主動與你的頁面發生了互動,那麼瀏覽器就會認為這個記錄是有意義的,從而不會被最佳化。
現在大家明白為什麼我們之前實現挽留彈窗時需要使用者點選一下才生效了吧。
總結
部分瀏覽器存在 history pruning
最佳化策略:
目的:
減少使用者需要點選返回按鈕的次數
避免使用者在無關緊要的中間頁面上浪費時間
提高整體瀏覽體驗的流暢度
常見的
可能
被最佳化的情況:重定向頁面: 瀏覽器可能會跳過HTTP重定向的中間頁面
短暫停留的頁面: 如果使用者在一個頁面上停留時間很短就導航到下一個頁面,這個短暫停留的頁面可能會被剪枝
動態生成的中間頁面: 例如,一些表單提交後的確認頁面
相似頁面: 如果連續訪問的幾個頁面非常相似,瀏覽器可能會合並它們
其他效能考慮:記憶體、層級過深等
注意,以上情況只是可能,具體表現因瀏覽器不同而各異,而且可能還有其他未知的策略,總之,具體策略細節其實不太重要,重要的是大家知道存在這麼個事,以後遇到類似問題時就不會一臉懵了。