天天看點

[極緻使用者體驗] 網頁裡的「傳回」應該用 history.back 還是 push ?1. 什麼是「傳回」按鈕?2. 什麼是 push、back、replace?3. 「傳回」按鈕的難題4. 網頁「傳回」按鈕,什麼效果才是符合使用者認知的?5. 結束語6. 寫在最後

大家好,我是公衆号「線下聚會遊戲」作者HullQin,開發了《聯機桌遊合集》,是個網頁,可以很友善的跟朋友聯機玩鬥地主、五子棋等遊戲。

1. 什麼是「傳回」按鈕?

這裡不是浏覽器的「傳回」按鈕,我們沒辦法修改它的行為。

而是網頁代碼中的「傳回」按鈕,我們可以定義它的行為。

舉個例子

比如我的五子棋小遊戲:

點開連結,會出現文章開頭圖檔的的頁面——遊戲首頁,「進入房間」後,左上角有個「離開房間」按鈕,點選後,會傳回首頁。

這種需要傳回上層頁面的按鈕,在本文中,稱之為「傳回」按鈕。

[極緻使用者體驗] 網頁裡的「傳回」應該用 history.back 還是 push ?1. 什麼是「傳回」按鈕?2. 什麼是 push、back、replace?3. 「傳回」按鈕的難題4. 網頁「傳回」按鈕,什麼效果才是符合使用者認知的?5. 結束語6. 寫在最後

2. 什麼是 push、back、replace?

push back replace
浏覽器行為 頁面會發生跳轉,并在目前浏覽記錄新增一條記錄(之後你可以按浏覽器「傳回」,回到跳轉前的頁面)。 頁面傳回上一條浏覽記錄(之後你可以按浏覽器「前進」,重新回到傳回前的頁面)。若浏覽器沒有上一條記錄,則什麼都不會發生。 頁面會發生跳轉,覆寫目前的浏覽記錄。(你按浏覽器「傳回」,無法回到跳轉前的頁面)
HTML DOM API: History

History.pushState()

History.back()

History.replaceState()

history@4 或 React Router@4或@5

history.push()

history.goBack()

history.replace()

React Router@6

navigate(url, { state, replace: false })

navigate(-1)

navigate(url, { state, replace: true })

這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 頁面層級

假設網站有這樣的結構:

[極緻使用者體驗] 網頁裡的「傳回」應該用 history.back 還是 push ?1. 什麼是「傳回」按鈕?2. 什麼是 push、back、replace?3. 「傳回」按鈕的難題4. 網頁「傳回」按鈕,什麼效果才是符合使用者認知的?5. 結束語6. 寫在最後

它是一個樹狀結構,每個頁面、子產品劃分非常清晰。

什麼是頁面層級?

同一層子結點,稱之為同一個「頁面層級」。(例如圖中子產品A、B、C就是同一層級)

4.2 基于此定義,我們可以提出這樣的産品原則:

  • 頁面跳轉(push)或前進(forward),隻允許相鄰頁面層級,從左往右跳轉。
  • 網頁裡的「傳回」按鈕(back),隻允許相鄰頁面層級,從右往左傳回。
  • 對于同一頁面層級的跳轉:可以限制,必須先傳回某結點的父結點,再進入該結點的兄弟結點。如果确實有快速跳轉的訴求,隻能用replace實作。
  • 不允許跨子產品的跳轉(如子產品A某頁面跳子產品B某頁面)。如果一定需要這種跳轉,隻能在新标簽頁打開。
  • 不允許跨層級的跳轉(如第2層級直接跳轉第4層級、或第4層級跳到第2層級)。如果一定需要這種跳轉,隻能在新标簽頁打開。

這樣,頁面整體跳轉邏輯,是非常清晰的,對于使用者而言,也容易了解你的邏輯。

4.3 為什麼這樣定義産品原則?

産品原則的目标:讓浏覽器的曆史記錄棧與網頁結構保持一緻:

  • 使用者進入更深的頁面層級,浏覽器的曆史記錄棧就增1。
  • 使用者傳回更淺的頁面層級,浏覽器的曆史記錄棧就減1。

而浏覽器原生的「傳回」,正是使浏覽器的曆史記錄棧回退1個。這樣兩種「傳回」就歸一了。

這件就解決了「3.2 方案二」中的問題,達到這樣的效果:

  • 保留使用者使用原生「傳回」的權利。
  • 使網頁「傳回」按鈕具有唯一目的地。

但網頁「傳回」按鈕還有個問題必須解決:若浏覽器目前曆史記錄棧為空,或曆史記錄棧的上個頁面并非該網頁的頁面,點「傳回」,應該也能傳回它的父頁面。

現在我告訴你,這個技術難點,是有解的!

4.4 實作方案

「傳回」按鈕,邏輯如下

  1. 判斷曆史記錄棧的上個頁面,是不是我的父頁面。
  2. 如果是我的父頁面,我就用

    history.back()

    ,使用浏覽器原生傳回行為。
  3. 如果不是我的父頁面,我就用

    history.replace()

    ,使目前頁面替換為我的父頁面。(不能用push,否則在父頁面傳回,回到了子頁面,是反直覺的)

難點:如何判斷曆史記錄棧的上個頁面,是不是我的父頁面。

問題:浏覽器基于安全性,不允許你讀取曆史記錄棧。

解決方案

隻要父頁面跳轉到子頁面時,攜帶個「辨別」,告知子頁面,跳轉來源。子頁面就知道了。

跳轉時的「辨別」,剛好可以用

history.pushState()

中的

state

來實作。

實作跳轉連結(即我上篇文章提到的Link Button)

隻要是内部跳轉,都封裝一個統一的元件。該元件允許定義跳轉目的地,而且會在

state

中攜帶「辨別」(如果你的網頁有帶自定義

state

的訴求,則還需要在該元件中組裝一下參數中的

state

和「辨別」,變成新的

state

)。

實作傳回連結(比如叫BackLinkButton)

擷取目前頁面的

state

,如果包含了「辨別」,則直接

history.back()

;否則,用

history.replaceState

(注意replace時不用帶「辨別」)。

其它問題

實際使用中,發現一個問題,我直接舉真實案例。

我的五子棋,聯機對戰模式,頁面分為3個層級:首頁、對戰房間、單機演練。按照如下流程操作:

  1. 使用者直接輸入網址進入第2層級(對戰房間),此時沒「辨別」。
  2. 使用者點「單機演練」,攜帶「辨別」,進入第3層級。
  3. 使用者點「傳回房間」,發現此頁面

    state

    有「辨別」,觸發浏覽器原生傳回,傳回第2層級。
  4. 使用者點「離開房間」(此頁面

    state

    沒「辨別」,會通過replace進入第1層級)。
  5. 使用者點「前進」,會直接到第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端使用者對原生「傳回」的依賴沒那麼重,你想剝奪就剝奪吧 😁

6. 寫在最後

繼續閱讀