HTML5 History API提供了一種功能,能讓開發人員在不重新整理整個頁面的情況下修改站點的URL。這個功能很有用,例如通過一段JavaScript程式碼區域性載入頁面的內容,你希望通過改變當前頁面的URL來反應出頁面內容的變化,這時該功能可以派上用場。
舉個例子,當使用者從首頁進入幫助頁面時,我們通過Ajax來載入幫助頁面的內容。然後這個使用者又轉到產品頁面,我們需要再一次通過Ajax請求來替換頁面的內容。當使用者想分享頁面的URL時,通過History API,我們可以改變頁面的URL來反應內容的修改,這樣不管是使用者分享還是儲存的URL都能和頁面的內容對應起來。
基本知識
要檢視這個API提供了哪些功能非常簡單,開啟瀏覽器的Developer Tools工具皮膚,然後在console中輸入history。如果你的瀏覽器支援History API,你將會看到這個物件下面附帶了很多方法。
注意其中的pushState和replaceState這兩個方法。我們可以在console中進行一些簡單的測試,來看看當我們使用這兩個方法時URL會發生什麼變化。稍後我們將分析這兩個方法中的所有引數,現在我們只需要關注最後一個引數:
history.replaceState(null, null, 'hello');
上面程式碼中的replaceState方法改變了當前頁面的URL,在後面新增了一個'/hello'。不過並沒有發出任何request請求,當前視窗仍然停留在之前的頁面。不過這裡有個問題,當你點選瀏覽器的後退按鈕時,頁面並不會回退到我們通過replaceState方法修改之前的那個URL,而是直接回退到了上一個頁面(即我們進入到這個頁面之前的那個頁面)。這是因為replaceState方法不會修改瀏覽器的history,它只是簡單地替換了位址列中的URL。
要解決這個問題我們需要使用pushState方法:
history.pushState(null, null, 'hello');
現在再點選瀏覽器的後退按鈕,你會發現它和你預想的效果一樣。因為pushState方法將我們傳給它的URL新增到瀏覽器的history中,從而改變了瀏覽器的history。假如我們將另外一個完整的站點URL傳遞給它會發生什麼情況呢?例如我們在baidu.com的首頁進行測試,然後在console中輸入下面的內容。
history.pushState(null, null, 'https://twitter.com/hello');
瀏覽器會報錯。因為傳遞給pushState方法的URl必須和當前頁面的URL屬於同一個源(即不能跨域),否則會有很大的安全漏洞,開發人員可能會借用該功能來欺騙使用者,讓他們覺得自己是在訪問一個完全不同的站點,而事實並非如此。
來看看傳遞給pushState方法的所有引數:
history.pushState([data], [title], [url]);
- 第一個引數用來傳遞我們需要的資料,當頁面的狀態發生變化時我們可以接收到該資料。如使用者點選瀏覽器的後退和向前按鈕。需要注意的是在Firefox中只允許傳遞最多640K的資料。
- 第二個引數title是一個字串,不過截止到目前,幾乎所有的瀏覽器都忽略該引數。
- 最後一個引數是我們想要替換的URL。
簡單回顧一下
這些History API最主要的功能就是不重新載入頁面。以往我們只能通過改變window.location的值來修改當前頁面的URL,不過這會導致整個頁面被重新載入。如果你修改的只是URL中的hash,則不會導致頁面被重新整理。
使用舊的hashbang方法可以改變頁面的URL而不重新整理頁面。著名的Twitter就是使用的該方法,不過也廣受詬病,畢竟hash在location中並不被作為一個真正的資源來對待。
作為History API的早期支持者,Twitter後來拋棄了傳統的hashbang方法。在2012年,Twitter的團隊介紹了他們的新方法,並列出了其中的一些問題同時還詳細地介紹了各瀏覽器應該如何實現該規範。
一個使用pushState和Ajax的例子
https://css-tricks.com/examples/State/
在該示例中,我們希望使用者通過我們的網站找到電影捉鬼敢死隊(一部美國電影)中的演員。當使用者選擇一個圖片時,我們需要在下方顯示該演員對應的文字描述,同時給該圖片一個被選中的效果。當點選後退按鈕時,頁面應該切換到上一個被選中的圖片狀態,同時圖片下方的文字也要一併切換。當點選前進按鈕時也一樣。
這裡有一個效果圖:
這個示例的HTML程式碼非常簡單:div.gallery中包含了所有的連結,每個連結裡有一個圖片。接下來我們放置了一個空的div.content,用來存放當演員圖片被點選時顯示在下放的文字。
<div class="gallery"> <a href="https://cdn.css-tricks.com/peter.html"> <img src="bill.png" alt="Peter" class="peter" data-name="peter"> </a> <a href="https://cdn.css-tricks.com/ray.html"> <img src="ray.png" alt="Ray" class="ray" data-name="ray"> </a> <a href="https://cdn.css-tricks.com/egon.html"> <img src="egon.png" alt="Egon" class="egon" data-name="egon"> </a> <a href="https://cdn.css-tricks.com/winston.html"> <img src="winston.png" alt="Winston" class="winston" data-name="winston"> </a> </div> <p class="selected">Ghostbusters</p> <p class="highlight"></p> <div class="content"></div>
如果沒有JavaScript該頁面仍然可以正常工作,點選圖片可以跳轉到對應的頁面,然後點選後退按鈕也可以回到之前的頁面。這是為了考慮頁面的可訪問行和優雅降級。
接下來我們要新增JavaScript程式碼了。我們通過event propagation給div.gallery元素中的每一個link新增一個事件處理程式,像這樣:
var container = document.querySelector('.gallery'); container.addEventListener('click', function(e) { if (e.target != e.currentTarget) { e.preventDefault(); // e.target is the image inside the link we just clicked. } e.stopPropagation(); }, false);
在if語句中,我們獲取到被選中圖片的data-name屬性的值,然後將'.html'新增到後面拼成一個要訪問的頁面地址,並將其作為第三個引數傳遞給pushState方法(不過在真實的例子中我們可能會在Ajax請求成功之後才會去修改URL)。
var data = e.target.getAttribute('data-name'), url = data + ".html"; history.pushState(null, null, url); // 此處更改當前的classes樣式 // 然後使用data變數的值更新 // 並通過Ajax請求.content元素的內容 // 最後再更新當前文件的title
(當然,此處我們也可以直接使用link的href屬性的值)
我將真實程式碼中的內容都替換成註釋了,這樣我們可以只關注pushState方法的使用。
現在我們點選圖片,URL和Ajax請求的內容會被自動更新,但是當我們點選後退按鈕時並不會回退到之前選中的演員圖片。這裡我們還需要在使用者點選後退和前進按鈕時使用另外一個Ajax請求來更新內容,並再一次使用pushState方法來更新頁面的URL。
我們使用pushState方法中的第一個引數(其中的state)來儲存狀態資訊:
history.pushState(data, null, url);
上面程式碼中的data引數在popstate事件觸發時可以被獲取到。當瀏覽器的後退和前進按鈕被點選時會觸發popstate事件。
window.addEventListener('popstate', function(e) { // e.state表示上一個被點選的圖片的data-attribute });
我們可以通過該引數傳遞一些我們需要的資訊,例如在該示例中我們將之前選中的捉鬼敢死隊的演員作為引數傳遞給requestContent方法,在該方法中,我們使用jQuery的load方法進行一次Ajax請求。
function requestContent(file) { $('.content').load(file + ' .content'); } window.addEventListener('popstate', function(e) { var character = e.state; if (character == null) { removeCurrentClass(); textWrapper.innerHTML = " "; content.innerHTML = " "; document.title = defaultTitle; } else { updateText(character); requestContent(character + ".html"); addCurrentClass(character); document.title = "Ghostbuster | " + character; } });
如果使用者點選了演員Ray的圖片,event listener會被觸發,然後在pushState事件中儲存圖片的data屬性的值。當使用者點選另外一個圖片,並點選了瀏覽器的後退按鈕,此時popstate事件會被觸發,從而重新載入ray.html頁面。
這意味著什麼呢?當我們點選一個演員的圖片然後將被更改的URL分享出去,使用者訪問這個URL時對應的HTML檔案會被自動載入進來。這會帶來一些更好的使用者體驗,並保證了URL和頁面內容的一致性從而減少了因此而帶給使用者的一些困惑。
上面的示例只是簡單地通過jQuery來動態載入內容,我們當然也可以在pushState方法中傳遞一些更加複雜的物件。不過這個例子已經能足夠說明問題並幫助我們開始學習如何使用History API的功能。我們先要學會走,然後才能跑。
下一步
如果我們想大範圍地使用這種技術,我們應該考慮使用一些專有的工具,例如pjax。 它是一個jQuery的外掛,使用它可以大大提高我們同時使用Ajax和pushState方法進行開發的速度,不過它只支援那些使用History API介面的現代瀏覽器。
History JS可以相容舊瀏覽器,對於不支援History API介面的瀏覽器,它依然使用舊的URL hash的方式來實現同樣的功能。
有關URLs
這裡我特別引用了Kyle Neath有關URLs的說明:
URLs是一個通用的概念,它可以工作在Firefox, Chrome, Safari, Internet Explorer, curl, wget,甚至在你的iPhone, Android以及便籤紙上。它是web中的一個通用的語法。不要認為這是理所當然的。任何一個稍微懂點技術的使用者都可以瀏覽你的應用的90%以上的部分而不用去刻意記住那些URL的結構。要實現這樣的效果,你需要考慮URLs的實用性。
這意味著不論你想要進行什麼樣的hacks或效能優化,作為web開發人員,你應該注重URL。而隨著HTML5 History API的幫助,我們可以輕鬆地解決諸如上述示例中的一些問題。
常見問題
- 將Ajax請求的地址嵌入到a標記的href屬性中通常是個不錯的主意。
- 確保在JavaScript的click事件處理程式中return true,這樣當有人使用中鍵點選或命令點選時不會導致程式被意外覆蓋。
補充
- Mozilla有關操作瀏覽器history的文件
- Ajax示例集錦Dive into HTML5
- Twitter有關pushState的實現
瀏覽器支援
Chrome | Safari | Firefox | Opera | IE | Android | iOS |
---|---|---|---|---|---|---|
31+ | 7.1+ | 34+ | 11.50+ | 10+ | 4.3+ | 7.1+ |