紀錄工作經驗、相關知識,解決技術相關問題。

Javascript, 新手 Jamp Holes, 網站相關

JS 上一頁與下一頁語法:history.back、forward 與 go 用法教學

JS 上一頁與下一頁語法:history.back、forward 與 go 用法教學 特色圖片

在前端開發與行動版網頁設計中,妥善運用 JS 上一頁 與瀏覽器歷史紀錄,能為使用者提供流暢的導覽體驗。當我們需要以程式碼模擬瀏覽器左上角的上一頁、下一頁按鈕功能時,主要會透過 JavaScript 的 History API 來達成。本文將完整整理 history.back()、history.forward() 與 history.go() 的實作方法、history.state 狀態攜帶技巧,以及 SPA 架構下的各種防呆機制與常見排錯方案。

JS 上一頁

在網頁中要實現返回上一頁的功能,主要有兩個常見的標準寫法:

history.back();

或者使用帶有數值參數的 history.go() 方法:

history.go(-1);

這兩種寫法在多數現代瀏覽器中的行為相同,都能直接觸發瀏覽器的歷史紀錄返回動作,效果等同於點擊瀏覽器的上一頁按鈕。

JS 下一頁

當使用者停留在歷史紀錄的中間頁面,需要前往下一頁時,可以使用 history.forward() 方法。

請注意確保語法拼寫正確,正確的下一頁寫法為:

history.forward();

若拼寫錯誤(例如少打字母),將無法正確觸發導覽動作。此外,若要透過數值參數來控制前往下一頁,亦可使用與 history.go(-1) 相對應的正向數值:

// 前往下一頁
history.go(1);

使用 history.go(1) 的效果與 history.forward() 相同,兩者皆能將瀏覽器視窗導向歷史紀錄中的下一頁。

指定往前幾頁與跨頁跳轉

若需要控制瀏覽器跨越多個頁面進行跳轉,同樣可以透過帶入整數參數的 history.go() 方法來達成。帶入負數代表往歷史紀錄的上方(上一頁方向)移動,帶入正數則代表往歷史紀錄的下方(下一頁方向)移動。

// 指定往前一頁(上一頁)
history.go(-1);

// 指定往前三頁
history.go(-3);

// 指定往後兩頁(下一頁的下一頁)
history.go(2);

透過這類彈性的數值設定,開發者可以針對特定的導覽列或使用者操作情境,精準控制網頁在瀏覽器歷史堆疊中的跳轉位置。

使用 history.pushState 與 history.replaceState 攜帶自訂 state 狀態

在開發單頁式應用程式(SPA)時,僅切換網址或觸發返回往往不足以還原複雜的使用者操作現場。透過 history.pushState() 與 history.replaceState(),開發者可以在瀏覽器歷史堆疊中寫入自訂的 state 物件、頁面標題(目前多數瀏覽器忽略此參數,通常帶空字串)與新的網址路徑。

以下為完整的寫入與狀態取值實作範例:

// 1. 當使用者點擊分頁或進行篩選時,將自訂 state 與新網址寫入歷史堆疊
const nextState = { pageId: 3, filter: 'active', scrollPosition: 250 };
const nextTitle = '';
const nextUrl = '/products?page=3';

history.pushState(nextState, nextTitle, nextUrl);

// 2. 當使用者點擊瀏覽器上一頁或下一頁時,透過 popstate 事件讀取帶入的 state
window.addEventListener('popstate', (event) => {
  if (event.state) {
    console.log('成功讀取歷史紀錄狀態:', event.state);
    console.log('目標頁面 ID:', event.state.pageId);
    console.log('篩選條件:', event.state.filter);
    
    // 根據取出的 state 狀態重新渲染畫面與還原捲軸位置
    renderPageWithState(event.state);
  } else {
    console.log('目前歷史堆疊無自訂 state,可能為初始頁面');
  }
});

若不需要在歷史堆疊中新增紀錄,僅想替換當前頁面的網址與狀態,則可使用 history.replaceState({ pageId: 3 }, '', '/products?page=3-updated')。

實務開發注意事項與防呆機制

在實際前端專案中,盲目呼叫 history.back() 或 history.go() 往往會隱藏著非預期的體驗風險。如果使用者是透過外部直接點擊連結、掃描 QR Code 或直接輸入網址進入該頁面(Direct Traffic),此時瀏覽器的歷史堆疊長度通常為 1。

若在此狀態下直接觸發返回動作,瀏覽器將會停留在原地,甚至可能跳轉到瀏覽器的預設空白頁或上一個瀏覽器分頁,造成使用者體驗中斷。因此,建議在執行返回或跳轉動作前,搭配 history.length 屬性進行條件判斷:

// 檢查瀏覽器歷史堆疊長度是否大於 1
if (history.length > 1) {
  history.back();
} else {
  // 若無歷史紀錄,則退回預設首頁或指定路徑
  window.location.href = '/';
}

為了讓開發者在實作時能更直覺掌握 history.go() 的數值與對應行為,以下整理 history.go() 的參數對照表:

參數值動作與方向說明
0 或無參數重新整理相當於點擊瀏覽器的重新整理按鈕 (Reload)
負整數(如 -1、-3)往歷史上方移動代表返回上一頁或指定的歷史頁數
正整數(如 1、2)往歷史下方移動代表前往下一頁或指定的未來頁數

傳統 MPA 與 SPA 架構下的 History 導覽行為差異對照表

比較維度傳統多頁式網站 (MPA)單頁式應用程式 (SPA)
頁面載入機制每次導覽或呼叫 history.back() 皆會向伺服器重新請求完整 HTML 與資源,整頁重新整理。網址改變時不重新載入整頁,透過 JavaScript 非同步抓取資料(Fetch/Axios)並局部抽換 DOM。
BFCache 與狀態保留瀏覽器自動將完整快取載入,但若伺服器資料變動需手動處理快取失效。需依賴 popstate 事件與 event.state 還原元件狀態與捲軸位置。
History API 角色主要用於簡單的頁面返回與前進導覽。核心路由核心,負責維持瀏覽器歷史與前端虛擬路由(Virtual Router)的高度同步。

SPA 架構下誤用原生 history.back() 導致 DOM 狀態不同步

在 Vue、React 或原生 Web Components 等單頁式應用(SPA)中,網頁實質上並未重新載入,而是透過 JavaScript 動態抽換畫面。

  • 踩坑情境:
    工程師在自訂的「上一頁」按鈕直接綁定 history.back()。當使用者在同一個頁面內進行多次前端換頁(例如:從 /products/1 切換到 /products/2,再到 /products/3),此時點擊返回,瀏覽器的網址與堆疊確實退回了 /products/2,但若前端元件的生命週期(Lifecycle)沒有正確監聽 popstate 事件來重新抓取 API 資料,畫面就會停留在 /products/3 的 DOM 結構,造成「網址變了,但畫面沒更新」的嚴重 Bug。
  • 正確排錯與實作解法:
    在 SPA 中,不應單純依賴全域的 history.back() 來處理業務邏輯,而應結合框架的路由器(如 Vue Router / React Router),或透過全域監聽 popstate 事件來同步觸發資料重撈:
// 監聽瀏覽器上一頁、下一頁按鈕觸發的歷史紀錄變動
window.addEventListener('popstate', (event) => {
  console.log('目前歷史堆疊狀態:', event.state);
  // 觸發重新取得該頁面資料的函式
  fetchPageDataByRoute(window.location.pathname);
});

惡意跨域跳轉與 History API 的安全限制

現代瀏覽器為了防止惡意網站透過指令惡意操控使用者的瀏覽歷史、甚至誘導使用者返回釣魚網站,對 History API 施加了嚴格的安全防護。

  • 踩坑情境:
    部分初學者誤以為可以透過 history.go() 或 history.pushState() 強制將使用者導向不同網域的歷史頁面。例如在自家網站內試圖跳轉到 history.go() 跨網域的歷史堆疊,結果遭到瀏覽器主控台噴出安全性阻擋錯誤。
  • 排錯原則:
    history.pushState() 與 history.replaceState() 的 url 參數受到嚴格的同源政策(Same-Origin Policy)限制。若傳入的網址與當前網頁不同網域(Domain)、通訊協定(Protocol)或連接埠(Port),瀏覽器會直接拋出 SecurityError 異常。

非同步載入畫面時連續點擊上一頁造成的「競態條件(Race Condition)」

  • 排錯案例:
    當使用者在短時間內連續點擊兩次自訂的返回按鈕(等同連續觸發兩次 history.back()),若頁面資料是透過 fetch 或 Axios 非同步載入,可能會發生「先發出的 API 請求較慢返回,後發出的請求較快返回」的狀況。這會導致畫面最終渲染出錯誤頁面的資料。
  • 防呆排錯解法:
    在處理歷史紀錄返回與資料非同步請求時,必須加入 AbortController 來終止前一次尚未完成的請求,確保畫面的資料一致性:
let currentAbortController = null;

async function loadHistoryPageData() {
  // 若有尚未完成的請求,先予以終止
  if (currentAbortController) {
    currentAbortController.abort();
  }
  
  currentAbortController = new AbortController();
  
  try {
    const response = await fetch('/api/get-page-data', {
      signal: currentAbortController.signal
    });
    const data = await response.json();
    renderPage(data);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('前一次的歷史頁面請求已被安全取消');
    } else {
      console.error('載入歷史頁面失敗', error);
    }
  }
}

window.addEventListener('popstate', loadHistoryPageData);

常見歷史紀錄問題排查與解決方案

瀏覽器歷史堆疊數值 history.length 不符合預期的原因與判斷機制

在實作返回上一頁功能時,開發者常以 history.length 判斷是否有歷史紀錄可供返回。然而,history.length 代表的是目前分頁在整段生命週期內累積的「堆疊總數」,並不完全等同於「當前頁面之前還有幾頁」。

  • 常見情境與技術盲點:
    若使用者從搜尋引擎進入頁面 A,接著依序瀏覽頁面 B、頁面 C,此時 history.length 為 3。若使用者手動點擊瀏覽器上一頁返回頁面 B,並在此頁面點擊新連結進入頁面 D,舊的頁面 C 堆疊將會被覆蓋,但總長度仍然維持在 3。
    更常見的狀況是,使用者在新分頁(target="_blank")開啟網頁,即使該網頁是從其他網站點擊進來,瀏覽器在新分頁的歷史堆疊起點長度也固定為 1。這會導致單純判斷 history.length > 1 時,將使用者導向錯誤的跳轉邏輯。
  • 排查與防呆改寫機制:
    若要精準判斷使用者是否為從站內其他頁面點擊進入(非直接開啟新分頁或輸入網址),建議結合 document.referrer 進行雙重檢查:
function handleSmartBack(fallbackUrl = '/') {
  const currentHost = window.location.host;
  const referrer = document.referrer;
  
  // 檢查是否有來源頁面,且來源頁面屬於同一個網域
  const isInternalReferrer = referrer && referrer.includes(currentHost);

  if (isInternalReferrer && history.length > 1) {
    history.back();
  } else {
    // 跨域來源或直接開啟新分頁時,退回指定預設頁面
    window.location.href = fallbackUrl;
  }
}

history.go(0) 與 location.reload() 的執行差異

在歷史紀錄控制中,history.go(0) 常被用來觸發頁面重新整理,但在瀏覽器的底層快取(Cache)處理機制上,它與 location.reload() 存在些許行為差異。

  • 機制對照與差異:
    • history.go(0):優先從瀏覽器快取(Disk Cache / Memory Cache)載入當前頁面資源,效果接近使用者按下 F5 重新整理。
    • location.reload():預設行為與 history.go(0) 類似,但在傳統瀏覽器支援度與語意明確度上較佳。若傳入 true 參數(location.reload(true),於舊版規範中),會強制瀏覽器向伺服器重新發送請求並繞過快取。
  • 排查建議與複製範本:
    若專案需求是單純重新整理當前歷史節點,優先建議採用語意明確的 location.reload();若需確保重新整理時清除狀態,可以搭配網址查詢參數(Query Parameters)進行快取打破(Cache Busting):
// 方式一:標準重新整理當前頁面
function refreshCurrentPage() {
  window.location.reload();
}

// 方式二:繞過瀏覽器快取的重新整理機制
function forceRefreshWithoutCache() {
  const url = new URL(window.location.href);
  url.searchParams.set('_t', Date.now());
  window.location.href = url.toString();
}

BFCache(往返快取)造成 history.back() 頁面狀態未更新排查

當使用者點擊上一頁回到前一個頁面時,現代瀏覽器(如 Chrome、Safari、Firefox)會啟用 BFCache(Back-Forward Cache) 機制。這會將前頁的完整 DOM 結構與 JavaScript 記憶體狀態直接保留,導致回到該頁時,DOMContentLoaded 或 load 事件都不會重新觸發。

  • 踩坑狀況:
    使用者在購物車頁面變更了商品數量,點擊結帳後,又點擊 history.back() 返回購物車。由於頁面從 BFCache 直接載入,畫面上的數量仍然是舊的資料,沒有更新。
  • 排查與解決步驟:
    必須透過監聽 pageshow 事件,並檢查 event.persisted 屬性來確認頁面是否來自 BFCache。若為 true,則手動執行資料重撈或畫面更新:
// 監聽頁面顯示事件(包含從 BFCache 返回)
window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('頁面來自 BFCache,開始更新畫面資料...');
    // 執行資料重新同步 logic
    syncPageData();
  }
});

function syncPageData() {
  // 重新取得最新狀態並渲染 DOM
  fetch('/api/get-cart-status')
    .then((res) => res.json())
    .then((data) => {
      updateCartUI(data);
    });
}

參考資料與延伸閱讀

發佈留言