我的烏拉拉練功坊

請來參觀移植到 Google Firebase 的成績 https://oolala.xyz/ken73chen/

2017年8月27日

document.location.hash 和 window.history.pushState()

來自 Descrier 原圖,CC BY 2.0 授權
「網址」很重要,有獨一無二的網址,使用者才可以 bookmark、分享;看起來像是廢話,不過因為網頁的內容如果是利用 AJAX 或類似的方式產生,就不見得有獨一無二的網址了。

所以後來 hash 成了 AJAX 的好搭檔,HTML5 之後,再加上 window.history.pushState()window.history.replaceState()、popstate 事件,就更方便了!

Hash

hash 就是 URL 中,# 後面的那一串東西…反正 hash 的命就是跟在 # 後面,嚴謹的講,就是用 document.location.hash 取得的部份。

在以往,hash 的功能,大概就是跳到網頁上的某個錨點,例如:

<a name='catalog'>catalog</a>
但是在 HTML5 裡面,不再使用 name 屬性,改用 id:

<a id='catalog'>catalog</a>
網址加上 hash,就可以讓網頁直接捲頁到指定的地方,https://www.foo.com/#catalog

有時候也被這樣用:

<a href='#' onclick='showSomething();'>show something</a>
後來 Gmail 出現,大家見識到 hash 搭配 AJAX 實在方便,而且還可以增加 URL 的可讀性,例如 https://mail.google.com/mail/u/0/#inbox/15e20b433feb28f5

hash 最大的特色,就是它雖然是網址的一部份,但是 hash 不會傳到對面的 server,所以應用在 AJAX 時,改變 hash 不會載入新網頁,只會觸發 hashchange 事件,到此,hash 至少有兩個作用:
  1. 當錨點使用,下回打開帶有錨點的網址,就會直接跳到網頁指定的地方
  2. 藉由 hashchange 事件,控制 JavaScript 的行為
來看 JSFiddle,因為 iframe 的關係,所以要用電腦看才看得到結果。

可以看到,按下不同更新速度的 link 後,基本上只更改了 iframe 網址的 hash 部份,所以網頁不會從頭 load,只會觸動 hashchange 事件,非常容易。

hashchange 的技術細節,請參看 MDN

window.history.pushState() 等


window.history.pushState()window.history.replaceState()、popstate 事件,這三個多半一起使用,在 MDN 上,稱呼這幾個是「操作瀏覽歷史」,pushState() 是在瀏覽歷史中加入一筆瀏覽紀錄,replaceState() 是以新的資料取代現在這一個瀏覽紀錄,popState 事件晚點說。

在應用上,如果你按了這一頁下方的按鈕,或者用滑動的方式,跳到較新或較舊的文章,可以很明顯的看的出來,是用 AJAX 的方式更新網頁內容的,網址列也跟著更改;如果從網址列的網址直接進來,server 送出來的就是該篇文章的內容,和 AJAX 換頁更新的結果是一樣的。

window.history.pushState(object,title,link),第一個 object 先不管它,剩下的參考以下的例子,雖然有用到 MooTools,不過相信是一看就懂:
// <a href='https://www.foo.com/somepage.html' class='ajaxLink' title='some title'>next article</a> $$('a.ajaxLink').addEvent('click',function(event) { event.preventDefault && event.preventDefault(); var link = this.getProperty('href'), title = this.getProperty('title'); getPageContent(link).then((content) => { updatePageContent(content); window.history.pushState({},title,link); document.title = title; }); });
這樣子,該更新的內容有更新、網址也有更新成 https://www.foo.com/somepage.html、title 也有更新,提醒:
  1. link 只可以改 pathname 以後的部份,例如網站是 https://www.foo.com,那麼可以改成 https://www.foo.com/somepage.html、https://www.foo.com/somepage.html?q=test,不可以改成 https://www.google.com、http://www.foo.com
  2. 要先用 history.pushState() 後,才可以用 document.title 更新 title
  3. window.history.pushState()history.pushState() 是一樣的結果
如果這時候使用者按下 F5 reload,會發生什麼事情呢?當然就是會重新載入 https://www.foo.com/somepage.html。

如果使用者 somepage.html => somepageelse.html => somepagemore.html 這樣一直看下去,在 somepagemore.html 按下瀏覽器的上一頁,會發生什麼事?

強調一下,是瀏覽器的上一頁,不是網頁上面做的上一頁按鈕。

這時候就會退回瀏覽器瀏覽歷史的上一個瀏覽歷史,也就是 somepageelse.html,但是這一筆瀏覽歷史是用 history.pushState() 放進來的,所以瀏覽器只會更改網址列的網址,並且產生 popState 事件,網頁的內容不會更改,也不會去跟 server 要 somepageelse.html 的內容,也就是依舊是 somepagemore.html 的內容。

所以要聽 popState 事件,順便稍微改一下 history.pushState(): <a href='nextpage.html' class='ajaxLink' title='nextpage's title'>next page</a> $$('a.ajaxLink').addEvent('click',function(event) { event.preventDefault && event.preventDefault(); var link = this.getProperty('href'), title = this.getProperty('title'), stateObj = {}; getPageContent(link).then((content) => { stateObj = { content: content, title: title, expire: Date.now() + 60*60*1000 }; updatePageContent(content); window.history.pushState(stateObj,title,link); document.title = title; }); }); window.addEvent('popState', function(event) { var state = event.event.state; if (state.expire && state.expire <= Date.now()) { updatePageContent(content); } else if (state.expire && state.expire > Date.now()) { getPageContent(document.location.href).then((content) => { updatePageContent(content);}); } else { window.location.reload(); } }); 不只是瀏覽器的上一頁會引發 popState 事件,瀏覽器的下一頁、history.go(n)history.back()history.forward() 當然也會,history.pushState() 第一個參數那個 stateObj 的內容,會跟著瀏覽紀錄放在使用者端的瀏覽器,在 Firefox 的限制是 JSON 序列化後,最長 640k,如果有必要,當然可以搭配例如 sessionStorage 使用。

有時候不會產生 popState 事件,例如瀏覽器關閉,再重新打開,這時候可以從 history.state 得到之前用 history.pushState()history.replaceState() 存入的 state。

技術細節,請參考 MDN

Google Analytics

如果有用 Google Analytics,在 history.pushState() 之後,要通知 Google Analytics: ga('send', { 'hitType': 'pageview', 'page': YOUR_NEW_URL });

DISQUS

如果有用 DISQUS:
DISQUS.reset({ reload: true, config: function () { this.page.identifier = NEW_IDENTIFIER; this.page.url = NEW_URL; } });

AddThis

如果有用 AddThis:
var addthis = document.body.getElement('div.addthis_inline_share_toolbox'); window['addthis_share'] = window['addthis_share'] || {}; window['addthis_share'].url = NEW_URL; window['addthis_share'].title = NEW_TITLE; addthis.setAttribute('data-url',NEW_URL); addthis.setAttribute('data-title',NEW_TITLE); addthis.setAttribute('data-description',NEW_DESCRIPTION); window.addthis.toolbox('.addthis_inline_share_toolbox');

既生瑜

有些文章認為,既然有了 history.pushState(),hash 就沒有用武之地了。

個人是覺得太極端了,至少以這篇文章使用 hashchange 的那一個例子來看,用 hash 實在是方便極了,否則,還得設計個 API 來用。