前言
既前篇回顧 Hash Anchor (錨點)在網頁的實作後,大家應該稍微了解瀏覽器可以透過 Anchor 快速切換到指定的網頁元件位置,也可以透過 上下頁切換觸發 onHashChange
快速恢復前後一個選擇的位置,甚至可以用來讓 mobile 體驗更好,且這些動作實際上 不會直接觸發「GET Request」事件
去跟伺服器要取 html 頁面資料。
而今天要來討論的是更有控制性的 History API,可以讓開發者不只是當前網址背後的 Hash 值,而是可以為所欲為的 控制整個網址路徑
要變成什麼樣子,以下我們先來了解 History API 得基本功能及案例,最後再來看看 React Router 內部是如何透過這個 API 製作出強大的功能,比如讓大家更容易開發 Single Page Application(SPA)。
History API 功能
透過 History API 可以觸發簡單控制並移動到特定歷史記錄
回上頁
window.history.back(); window.history.go(-1);
去下頁
window.history.forward(); window.history.go(1);
當前有幾個上下頁
window.history.length;
在 HTML5 時提供了更進階的功能 pushState
及 replaceState
的功能,目前也幾乎是用市面上所有使用者的瀏覽器,以下就來介紹這兩個功能吧。
Histroy pushState
這個功能很像 Hash Anchor 被點擊時,會新增一個歷史紀錄是「當前網址 + Hash」,不一樣的是 pushState 有更多可以控制的項目
- 自訂網址路徑
- 儲存狀態物件
var stateObj = { foo: "bar" };
var pageName = "page 2";
var pagePath = "/home?params=1";
history.pushState(stateObj, pageName, pagePath);
Hisotry replaceState
與 pushState 很像,差別在 replaceState 不會新增一個歷史頁面,而是直接把當前頁面覆蓋,其外參數都一樣
var stateObj = { foo: "" };
var pageName = "page 2";
var pagePath = "/home?params=2";
history.pushState(stateObj, pageName, pagePath);
popstate 事件
觸發時機:每次網頁歷史紀錄被更新時,如果這個歷史紀錄是上述兩個由開發者自行定義的紀錄將可以取得 state Obj,但這時網站將不會有任何重 load 動作,需要自行定義需要處理什麼事。
如果能看懂以下範例,應該就已經初步了解 pushState、replaceState、popstate 三者的關係了
window.onpopstate = function(event) { alert("location: " + document.location + ", state: " + JSON.stringify(event.state)); }; history.pushState({page: 1}, "title 1", "?page=1"); history.pushState({page: 2}, "title 2", "?page=2"); history.replaceState({page: 3}, "title 3", "?page=3"); history.back(); // 跳出 "location: http://example.com/example.html?page=1, state: {"page":1}" history.back(); // 跳出 "location: http://example.com/example.html, state: null history.go(2); // 跳出 "location: http://example.com/example.html?page=3, state: {"page":3}
也可以透過 hisotry.state 取得當前頁面的 state Obj
var currentState = history.state;
案例分享
開頭先講一個熟悉的,也就是上篇提到的 Hash Anchor 的實作,我認為 Anchor 是瀏覽器內建透過 History API 往上蓋一層 API 實作出來的,以下我們使用 History API 看是否能達到同樣的效果。
配置解說
- 頁面上方 Nav
呈現網址
及串接History back、forward
控制轉頁,讓大家可以好測試 - 點擊 Nav 上連結時,是自行透過 js handle
控制網址切換
、頁面到指定 anchor 位置 當 onpopstate 事件發生時
,也是自己 handle 讓頁面到指定 anchor 位置第一次進入到頁面時
也會抓網址上是否有 anchor 參數,讓頁面到指定 anchor 位置
拆段落解說一下
- 頁面上方 Nav
呈現網址
及串接History back、forward
控制轉頁,讓大家可以好測試
let goLastPage = () => {
window.history.back();
}
let goNextPage = () => {
window.history.forward();
}
let updateNavInfo () => {
document.getElementById('historyLengthDisplay').innerText = `history length ${window.history.length}`;
document.getElementById('urlDisplay').innerText = location.href;
}
document.getElementById('lastPage').addEventListener("click", goLastPage, false);
document.getElementById('nextPage').addEventListener("click", goNextPage, false);
setInterval( updateNavInfo, 100)
- 點擊 Nav 上連結時,是自行透過 js handle
控制網址切換
、頁面到指定 anchor 位置
let scrollToAnchorId = (anchorId) => {
const anchorPos = document.getElementById(anchorId).getBoundingClientRect();
const bodyPos = document.querySelector('body').getBoundingClientRect();
const absTargetPosY = anchorPos.y - bodyPos.y;
window.scrollTo(0, absTargetPosY);
}
let changeUrl = (anchorId) => {
let state = { anchorId: anchorId};
let pageName = `Anchor ${anchorId}`;
let newUrl = location.href.split('?')[0] + `?anchor=${anchorId}`;
window.history.pushState(state, pageName, newUrl);
}
let positionChange = (e) => {
const anchorId = e.target.dataset.anchor_id;
scrollToAnchorId(anchorId);
changeUrl(anchorId);
}
document.getElementById('section1Anchor').addEventListener("click", positionChange, false);
document.getElementById('section2Anchor').addEventListener("click", positionChange, false);
document.getElementById('section3Anchor').addEventListener("click", positionChange, false);
document.getElementById('section4Anchor').addEventListener("click", positionChange, false);
document.getElementById('section5Anchor').addEventListener("click", positionChange, false);
當 onpopstate 事件發生時
,也是自己 handle 讓頁面到指定 anchor 位置
let onPopState = (e) => {
let anchorId = e.state.anchorId;
// scrollToAnchorId(anchorId)
// hard code for avoid browser behavior
setTimeout( () => {
scrollToAnchorId(anchorId)
}, 100);
}
window.onpopstate = onPopState;
第一次進入到頁面時
也會抓網址上是否有 anchor 參數,讓頁面到指定 anchor 位置
let onLoad = (e) => {
// 取得網址上的 anchor= 之後到 &或結尾 之前的字串
let matchArr = location.href.match(/anchor=(((?!\&).)*)/)
if(matchArr && matchArr.length > 1){
let anchorId = matchArr[1];
scrollToAnchorId(anchorId)
}
}
window.onload = onLoad;
案例介紹:曾用 History API 想做 SPA 頁面的辛苦談
連結:https://www.chickpt.com.tw/
這是我上一份工作打造的產品「小雞上工」頁面,這是一個以 APP 為主打市場的「全台最大媒合打工平台」,當時工作接到了要重新打造網頁首頁取代單純介面頁,讓產品能優先在網頁就被 體驗
並讓更多用戶導往「 App 平台」,也希望這個 體驗跟使用 APP 盡量一樣
,列出了以下個需求。
- 切換篩選條件不要跳轉頁面 =>
以 ajax 取資料重 render
- 分享網址時,進來的人也能看到同篩選畫面 =>
pushState
或replaceState
更新網址 - 回上一頁可以看到原先內容 =>
onpopstate
中取上一次 ajax response 內容或者儲存篩選條件重新發 ajax render 頁面
小結
原本以為這篇就要把 React Router 跟 History API 講完拉,結果一不小心範例寫著寫著就一直補充,希望有讓大家更清楚理解 History API 可以怎麼運在專案上,明天時再來細讀 React Router 是怎麼使用 History API 的。