在前端開發與行動版網頁設計中,妥善運用 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);
});
}
- JS 上一頁
- JS 下一頁
- 指定往前幾頁與跨頁跳轉
- 使用 history.pushState 與 history.replaceState 攜帶自訂 state 狀態
- 實務開發注意事項與防呆機制
- 常見歷史紀錄問題排查與解決方案
發佈留言