最近搞了下列表頁請求的功能,並做了一下調研整理,記此文備忘。
列表頁請求的功能到處可見,比如在博客園。
點擊相應的頁碼,頁面返回相應的內容,看上去似乎大同小異,但是一些小的細節還是可以區分優劣。
full load
公司原來的代碼采用的是 full load 的方式,也就是每點擊一次,頁面完全加載。並不只有我們網站這樣做,很多大廠也這樣搞,比如 新浪。
列表頁中的很多部分內容,其實都是一樣的,這樣做就每次需要重新加載這部分的內容,沒有必要,而且 css、js 都需要重新加載(雖然可能有緩存)。以前我逛學校的論壇,是用 PHP 的 Discuz! 搭建的,每個主題后的回復頁之間的跳轉都是 full load 的方式,體驗很差。
所以個人覺得,不管是性能還是用戶體驗上,full load 的方式在現在的 web 開發中,都是不可取的。
ajax
接着 ajax 出現了。ajax 就不多做介紹了,局部刷新,體驗非常好。但是單純的 ajax 雖然性能上比 full load 提高了不少,用戶體驗還不是很好,主要有以下兩點。
- 保存不了書簽
- 不支持瀏覽器的后退前進操作
究其根本,是因為傳統的 ajax 操作不改變 url。
ajax + #
為了解決以上問題,聰明的開發者們用 # 來改善體驗。
以博客園為例,我們請求第二頁的時候,實際的 url 是 http://www.cnblogs.com/#p2,當點擊頁碼發送請求時,同時改變頁面的 url,因為改變的是 lcation.hash,所以頁面並不會重載。我們將其保存為書簽,當我們打開 http://www.cnblogs.com/#p2 時,我們可以提取 hash 值,據此發起相應的 ajax 請求。
接着我們來看第二個需求,如何支持瀏覽器的后退前進操作。有些童鞋可能會問,已經有了上一頁、下一頁的功能,支持瀏覽器的后退前進操作,有必要嗎?灰常有必要,比如我們先點了第二頁,然后點了第四頁,我需要回到第二頁,又忘了剛才點的是第幾頁,會條件反射地去點瀏覽器的回退。其實博客園 http://www.cnblogs.com/ 沒有做這個功能,如何支持?我們可以監聽 hashchange
事件,當 url 的 hash 值發生變化時,重新發送請求。但是 hashchange
事件並不支持某些 IE (http://caniuse.com/#search=hashchange),對於不支持的瀏覽器,我們只能設置一個定時器,不斷得去查看頁面的 hash 是否改變,會造成不小的性能問題(或者直接放棄這部分瀏覽器,或者降級處理)。
簡單地寫了個 demo,猛戳 https://github.com/hanzichi/practice/tree/master/2016/pjax/hash 查看(沒有兼容不支持 hashchange 事件的瀏覽器)。
這里再插點題外話,講點小歷史。以前的搜索引擎爬蟲,是不會抓取 ajax 請求的內容的(畢竟木有這么智能),只會去抓網頁的源代碼,這就蛋疼了,我們既希望用 ajax 改善體驗,又希望內容可以被搜索引擎爬蟲抓取,二者不可得兼?Google 搜索制定了一套規則。
- 網站提交 sitemap 給 Google;
- Google 發現 URL 里有 #! 符號,例如example.com/#!/detail/1,於是 Google 開始抓取 example.com/?_escaped_fragment_=/detail/1;
_escaped_fragment_ 這個參數是 Google 指定的命名,如果開發者希望把網站內容提交給 Google,就必須通過這個參數生成靜態頁面。
也就是說,每個 ajax 請求的內容,都需要提供一個相同內容的靜態頁面,供爬蟲爬取。
隨着 web 的發展,這一切已經成為了歷史,現在的爬蟲已經可以執行 JavaScript,爬取 ajax 請求的內容了!這部分的內容,就不展開了,有興趣的可以參考下以下鏈接。
- Understanding web pages better
- Updating our technical Webmaster Guidelines
- 我們將棄用 AJAX 抓取方案
- AJAX Crawling (Deprecated)
- Making AJAX applications crawlable
ajax + pushState
ajax + #,似乎可以滿足一般的需求了,但是如果不止限於列表請求呢?改變 hash 值搞的 URL 看起來不像一個正常的 URL,而且 hash 本來的用處並非如此,這樣搞有點黑科技的感覺。HTML5 的出現,能讓 ajax 變的更加優雅。
為了解決傳統的 ajax 帶來的問題,HTML5 里加強了 history API,加入了 pushState、replaceState 接口和 popstate 事件。
舉個簡單的例子,我們看 GitHub,首先定位到頁面 https://github.com/hanzichi/underscore-analysis,然后點擊該 repo 下第一個行第一個文件夾 『underscore-1.8.3.js』,URL 變為 https://github.com/hanzichi/underscore-analysis/tree/master/underscore-1.8.3.js,頁面局部刷新,看了下 Network 面板,是一個 ajax 請求,且該操作支持保存書簽、回退前進等功能。這一切的實現都基於 history 新增的 API。
history 原有的 API 大都灰常簡單,比如 history.length
(該 tab 訪問過的網頁數量,新建 tab 時的空標簽該屬性值為 1),history.back()
,history.forward()
,history.go(-1)
等等,不多加介紹,簡單介紹下新增的 history.pushState
,history.replaceState
以及 popstate
事件。
history.pushState 方法接受三個參數,依次為:
- state:一個與指定網址相關的狀態對象,popstate 事件觸發時,該對象會傳入回調函數。如果不需要這個對象,此處可以填 null。history.state 屬性能保存當前頁面的 state 對象。
- title:新頁面的標題,但是所有瀏覽器目前都忽略這個值,因此這里可以填 null。
- url:新的網址,必須與當前頁面處在同一個域。瀏覽器的地址欄將顯示這個網址。
假設現在的網頁是 http://localhost/1.htm,我們使用 pushState 方法在瀏覽記錄(history 對象)中添加一個新紀錄。
var stateObj = {page: 2 };
history.pushState(stateObj, "page 2", "2.htm");
瀏覽器地址欄立刻顯示 <localhost/2.htm>,但是並不會跳到 2.htm 的頁面(pushState 不會觸發頁面刷新),甚至這個頁面不存在也不會報錯,它只是成為了瀏覽器中的最新記錄,可以查看 history.length,會發現該屬性值增加了 1。如果這時點擊倒退,url 將顯示 1.htm,內容不變。
如果 pushState 的 url 參數,設置了一個當前網頁的 # 值(hash),並不會觸發 hashchange 事件。如果設置了一個非同域的網址,則會報錯。
history.replaceState 方法的參數和 pushState 一樣,區別是它修改瀏覽器歷史中當前頁面的值,即使用 replaceState,history.length 並不會增加,只是替換了當前頁面在 history 中的記錄。
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(); // url 顯示為http://example.com/example.html?page=1
history.back(); // url 顯示為http://example.com/example.html
history.go(2); // url 顯示為http://example.com/example.html?page=3
前面 ajax + # 的例子中,我們用 hashchange 去判斷瀏覽器的前進后退操作,那么,是否有原生的監聽瀏覽器前進回退操作的事件呢?有的,popstate 事件。每當同一個文檔的瀏覽歷史(即 history 對象)出現變化時,就會觸發 popstate 事件,只有當用戶手動點擊瀏覽器后退前進按鈕,或者調用 back、forward、go 方法時才會觸發。
window.onpopstate = function(e) {
console.log(e.state);
// 等價於
// console.log(history.state);
}
於是我們要實現一個列表頁請求的功能,就呼之欲出了。點擊頁碼,用 pushState 塞入一條新的記錄,同時改變 url,然后發送 ajax 請求,局部更新。點擊瀏覽器后退前進按鈕,觸發 popstate 事件,發送請求,局部更新,請求的字段,可以根據 url 去判斷,也可以儲存在 state 中。寫了個簡單的 demo,猛戳 https://github.com/hanzichi/practice/tree/master/2016/pjax/pushState 查看(根據 url 判斷了)。
pushState vs #
ajax + pushState 以及 ajax + hash 的作用類似,但是推薦使用前者,有以下幾個優勢:
- pushState 能改變的 URL 的范圍大,在一個域名下的都可以,而 hash 的方法,因為 URL 只能改變 location.hash 的值,所以 URL 其實是只能在一個文檔(document)下改變。比如 GitHub 中的路由,用 hash 去做,就會很麻煩,而且也很丑
- 插入一條新的 history 記錄,用 pushState,不一定要改變 URL,而 hash 必須改變當前的 URL(精確地說是當前文檔的 hash 值)
- 毫無疑問,我們需要把一些數據存儲起來,在頁面上提取,然后發起相應的 ajax。用 pushState 的方法,我們可以把數據存在 history.state 中,也可以根據 URL 去判斷;而 hash 法只能改變 URL,根據 URL 判斷(准確說是根據 hash 值判斷)
- 目前瀏覽器還不支持 pushState 的 title 參數,一旦支持,就可以被利用;而 hash 法是無法改變 title 的。
pjax
上面只是個簡單的例子,如果要是實際生產環境中使用,大可用封裝過的插件。
pjax = pushState + ajax,GitHub 使用的就是封裝過的 pjax 插件。
- https://github.com/defunkt/jquery-pjax (GitHub 使用)
- https://github.com/welefen/pjax (welefen 基於上面那個插件改造的,不過好像已經不維護了)
使用方式可以參照相應的 README,不多做介紹了。