來自 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 至少有兩個作用:
- 當錨點使用,下回打開帶有錨點的網址,就會直接跳到網頁指定的地方
- 藉由 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 也有更新,提醒:
- 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
- 要先用 history.pushState() 後,才可以用 document.title 更新 title
- window.history.pushState()、history.pushState() 是一樣的結果
如果這時候使用者按下F5reload,會發生什麼事情呢?當然就是會重新載入 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 來用。