大家好,我是公衆号「線下聚會遊戲」作者HullQin,開發了《聯機桌遊合集》,是個網頁,可以很友善的跟朋友聯機玩鬥地主、五子棋等遊戲。
1. 什麼是「傳回」按鈕?
這裡不是浏覽器的「傳回」按鈕,我們沒辦法修改它的行為。
而是網頁代碼中的「傳回」按鈕,我們可以定義它的行為。
舉個例子
比如我的五子棋小遊戲:
點開連結,會出現文章開頭圖檔的的頁面——遊戲首頁,「進入房間」後,左上角有個「離開房間」按鈕,點選後,會傳回首頁。
這種需要傳回上層頁面的按鈕,在本文中,稱之為「傳回」按鈕。

2. 什麼是 push、back、replace?
push | back | replace | |
---|---|---|---|
浏覽器行為 | 頁面會發生跳轉,并在目前浏覽記錄新增一條記錄(之後你可以按浏覽器「傳回」,回到跳轉前的頁面)。 | 頁面傳回上一條浏覽記錄(之後你可以按浏覽器「前進」,重新回到傳回前的頁面)。若浏覽器沒有上一條記錄,則什麼都不會發生。 | 頁面會發生跳轉,覆寫目前的浏覽記錄。(你按浏覽器「傳回」,無法回到跳轉前的頁面) |
HTML DOM API: History | | | |
history@4 或 React Router@4或@5 | | | |
React Router@6 | | | |
這3種,都可以實作頁面跳轉,對于使用者體驗也是有差異的。
3. 「傳回」按鈕的難題
「傳回」按鈕,做好使用者體驗,挺難的。這裡羅列一些容易想到的、但不完美的方案。
3.1 方案一:用back實作「傳回」
存在的問題:
- 如果使用者直接從URL進入該頁面,點「傳回」無效。
- 同一個頁面,如果來源不同,點「傳回」,回到的頁面也不同,會讓使用者困惑。
其實,如果用back實作「傳回」按鈕,這個按鈕元素會有點多餘,因為它與浏覽器原生的「傳回」能力一樣。
3.2 方案二:用push實作「傳回」
這種方式解決了back導緻的2個問題,但并不完美。
存在的問題:
- 頁面浏覽記錄棧膨脹迅速,剝奪了使用者使用原生「傳回」按鈕的權利。
我解釋一下。比如有個
初始頁面H
,使用者從
初始頁面H
跳轉到了
清單頁A
,使用者通過點選
清單頁A
裡面的
詳情Ax連結
(x代表一個正整數,清單頁通常有多個詳情連結),可以進入
詳情頁Ax
。在
詳情頁Ax
中,可以點
網頁「傳回」按鈕
,回到
清單頁A
。
當使用者在
清單頁A
和
詳情頁Ax
之間多次通過
詳情Ax連結
和
網頁「傳回」按鈕
來回切換時,頁面浏覽記錄已經累積很多了,使用者若想通過浏覽器
原生「傳回」按鈕
,再傳回
初始頁面H
,是需要按很多次傳回的。
但使用者沒有這個耐心。
是以你不得不在
清單頁A
增加一個
網頁「傳回」按鈕
,用于跳轉
初始頁面H
。這就誕生了新的問題:
- 如果一個
的來源,不止清單頁A
,還有多個頁面可以跳轉初始頁面H
,那麼清單頁A
的清單頁A
,應該傳回到哪裡呢?網頁「傳回」按鈕
除此之外,我想強調一句:
剝奪使用者使用原生「傳回」按鈕的權利,不是一件好事。
尤其是對于安卓端使用者,重度依賴原生「傳回」操作(在螢幕邊緣左滑或右滑)。網頁打破了他們的操作習慣,隻能表明網頁使用者體驗做的不夠好。
4. 網頁「傳回」按鈕,什麼效果才是符合使用者認知的?
這裡,我想先提出「頁面層級」的概念。
4.1 頁面層級
假設網站有這樣的結構:
它是一個樹狀結構,每個頁面、子產品劃分非常清晰。
什麼是頁面層級?
同一層子結點,稱之為同一個「頁面層級」。(例如圖中子產品A、B、C就是同一層級)
4.2 基于此定義,我們可以提出這樣的産品原則:
- 頁面跳轉(push)或前進(forward),隻允許相鄰頁面層級,從左往右跳轉。
- 網頁裡的「傳回」按鈕(back),隻允許相鄰頁面層級,從右往左傳回。
- 對于同一頁面層級的跳轉:可以限制,必須先傳回某結點的父結點,再進入該結點的兄弟結點。如果确實有快速跳轉的訴求,隻能用replace實作。
- 不允許跨子產品的跳轉(如子產品A某頁面跳子產品B某頁面)。如果一定需要這種跳轉,隻能在新标簽頁打開。
- 不允許跨層級的跳轉(如第2層級直接跳轉第4層級、或第4層級跳到第2層級)。如果一定需要這種跳轉,隻能在新标簽頁打開。
這樣,頁面整體跳轉邏輯,是非常清晰的,對于使用者而言,也容易了解你的邏輯。
4.3 為什麼這樣定義産品原則?
産品原則的目标:讓浏覽器的曆史記錄棧與網頁結構保持一緻:
- 使用者進入更深的頁面層級,浏覽器的曆史記錄棧就增1。
- 使用者傳回更淺的頁面層級,浏覽器的曆史記錄棧就減1。
而浏覽器原生的「傳回」,正是使浏覽器的曆史記錄棧回退1個。這樣兩種「傳回」就歸一了。
這件就解決了「3.2 方案二」中的問題,達到這樣的效果:
- 保留使用者使用原生「傳回」的權利。
- 使網頁「傳回」按鈕具有唯一目的地。
但網頁「傳回」按鈕還有個問題必須解決:若浏覽器目前曆史記錄棧為空,或曆史記錄棧的上個頁面并非該網頁的頁面,點「傳回」,應該也能傳回它的父頁面。
現在我告訴你,這個技術難點,是有解的!
4.4 實作方案
「傳回」按鈕,邏輯如下
- 判斷曆史記錄棧的上個頁面,是不是我的父頁面。
- 如果是我的父頁面,我就用
,使用浏覽器原生傳回行為。history.back()
- 如果不是我的父頁面,我就用
,使目前頁面替換為我的父頁面。(不能用push,否則在父頁面傳回,回到了子頁面,是反直覺的)history.replace()
難點:如何判斷曆史記錄棧的上個頁面,是不是我的父頁面。
問題:浏覽器基于安全性,不允許你讀取曆史記錄棧。
解決方案
隻要父頁面跳轉到子頁面時,攜帶個「辨別」,告知子頁面,跳轉來源。子頁面就知道了。
跳轉時的「辨別」,剛好可以用
history.pushState()
中的
state
來實作。
實作跳轉連結(即我上篇文章提到的Link Button)
隻要是内部跳轉,都封裝一個統一的元件。該元件允許定義跳轉目的地,而且會在
state
中攜帶「辨別」(如果你的網頁有帶自定義
state
的訴求,則還需要在該元件中組裝一下參數中的
state
和「辨別」,變成新的
state
)。
實作傳回連結(比如叫BackLinkButton)
擷取目前頁面的
state
,如果包含了「辨別」,則直接
history.back()
;否則,用
history.replaceState
(注意replace時不用帶「辨別」)。
其它問題
實際使用中,發現一個問題,我直接舉真實案例。
我的五子棋,聯機對戰模式,頁面分為3個層級:首頁、對戰房間、單機演練。按照如下流程操作:
- 使用者直接輸入網址進入第2層級(對戰房間),此時沒「辨別」。
- 使用者點「單機演練」,攜帶「辨別」,進入第3層級。
- 使用者點「傳回房間」,發現此頁面
有「辨別」,觸發浏覽器原生傳回,傳回第2層級。state
- 使用者點「離開房間」(此頁面
沒「辨別」,會通過replace進入第1層級)。state
- 使用者點「前進」,會直接到第3層級。不符合預期。
為了解決這個情況,我做了相容處理:
如果目前頁面
state
沒「辨別」,如果目前浏覽器曆史記錄棧長度為1,直接replace是沒問題的,不會出現上述問題;但如果目前浏覽器曆史記錄棧長度大于1,我調用replace後,需要連續調用一次push和一次back,目的是清空浏覽器「前進」的曆史記錄棧。
打開網址 https://game.hullqin.cn/wzq/bgzyyds ,會直接進入第2層級。你可以按上述流程操作下。你不會遇到問題,因為這個問題已經被解決了,體驗好很多。
代碼片段參考
這是
LinkButton
邏輯,其中
back
參數,
true
表示是傳回按鈕,
false
表示是跳轉按鈕。我的
state
中「辨別」叫做
keepSession
。
if (back) {
return (
<BackLink to={to}>
{children}
</BackLink>
);
}
return (
<Link to={to} state={{ ...state, keepSession: true }} onClick={handleClick}>
{children}
</Link>
);
這是
BackLink
核心邏輯(注:
navigate
是React Router@6提供的函數)
const handleClick = (event) => {
if (event.button !== 0) return;
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
if (keepSession) {
navigate(-1);
} else if (window.history.length === 1) {
navigate(to, { replace: true });
} else {
navigate(to, { replace: true });
// 通過下面方式重新整理浏覽器"前進"記錄,以免通過"前進"進入不符預期的頁面
navigate(to);
navigate(-1);
}
};
return (
<Link to={to} onClick={handleClick}>
{children}
</Link>
);
如果你好奇
event.xxxKey
、
event.preventDefault()
那3行代碼,請一定要看下這篇文章:《你的 Link Button 能讓使用者選擇新頁面打開嗎?》
5. 結束語
另一個問題:多頁面應用
如果你的父頁面和子頁面,不是同一套前端代碼,而是兩套前端代碼。也就是說,它整體不是單頁面應用(SPA),而是多頁面應用(MPA),該怎麼辦呢?
請繼續閱讀:《多頁面應用裡,「網頁内傳回」按鈕,何時用 history.back 何時用 replaceState?》。
聊聊天
隻要你的頁面裡,沒有「傳回」按鈕,那啥事都沒有 😁
如果你的頁面,不追求移動端的極緻使用者體驗,那也沒啥事,PC端使用者對原生「傳回」的依賴沒那麼重,你想剝奪就剝奪吧 😁