本文介紹如何使用backbone的history模塊實現SPA應用里面的URL管理。SPA應用的核心在於使用無刷新的方式更改url,從而引發頁面內容的改變。從實現上來看,url的管理和頁面內容的管理是其中的兩個難點。就url的管理而言,主要有以下三方面的要求:
1)對於要采用單頁跳轉的鏈接,不能有頁面刷新;
2)瀏覽器的前進和后退,都能像多頁應用那樣,顯示之前訪問地址對應的內容;
3)應用處於任何一個單頁鏈接地址時,當用戶刷新,依然能初始化顯示該地址對應的內容。
假如要自己來實現一個能夠滿足以上三方面要求的功能,思路有2種。
第一種是利用錨點鏈接及hashchange,將所有單頁鏈接地址全部配置成錨點鏈接的形式,然后在hashchange事件里面,根據頁面當前的錨點值,執行不同的回調函數,用於更改頁面內容。這個思路在上一篇博客中《理解瀏覽器歷史記錄(2)-hashchange、pushState》給出了一個簡單的實現(demo),代碼雖然比較簡陋,但是也說明思路是可行的。
第二種是利用pushState,詳細步驟如下:
1)在點擊鏈接的時候,如果這個鏈接是單頁形式的鏈接,可通過pushState或者replaceState的方式來改變url;由於pushState跟replaceState並不會觸發popstate事件,所以在必要的條件下,還得在調用完pushState和replaceState調用完后,手動調用相應的回調函數;
2)監聽瀏覽器的popstate事件,這樣就能響應瀏覽器前進后退的操作,然后根據操作后的頁面地址找到對應的回調函數執行;
3)頁面初始化時,直接根據當前地址執行對應的回調函數即可。
上次也沒有給出簡單使用pushState實現SPA的例子,這次補上,功能與hashchange那個類似,就是寫法稍微有點不同而已。demo地址(不可測刷新操作,如果使用pushState,單頁地址刷新會報404,需在后台處理才能解決,此處畢竟只是個靜態頁):
http://liuyunzhuge.github.io/blog/pushState/demo7.html
比較起來,用hashchange做spa的方法要簡單一些,而且兼容性更好,加上mdn上給出的用定時器來模擬hashchange的方法,基本上用它是可以實現全瀏覽器兼容的SPA應用了。不過hashchange相比pushstate也有一些大的弊端:
一是url對自己不夠友好,本來在多頁應用里面,url可能都是絕對路徑或者相對路徑開頭的形式,如果全換成#hash的形式,顯然違背自己多年的使用習慣;
二是url對搜索引擎不夠友好。
所以要是能夠把hashchange,pushstate結合起來實現SPA里面的url管理,顯然這個事情就變得很完美了。事實上已經有很多的所謂的路由框架做到這一點,我也沒有去研究別的,加上一直對backbone這個框架的評價不錯,所以就琢磨着怎么用它實現我所需要的SPA的url管理了。
下面的內容就是介紹如何使用backbone的history模塊來實現spa的url管理。由於只是介紹方法,所以對相關的api的使用不會一一進行說明。感興趣地可以查閱backbone的官方文檔,主要是Router和History模塊的部分。要是想詳細了解瀏覽器如何管理歷史記錄,以及hashchange和pushstate使用要點的話,也可以看看我前面兩篇博客寫的一些基礎知識總結,它們對於我理解Backbone的history模塊還是很有幫助。
理解瀏覽器歷史記錄(2)-hashchange、pushState;
理解瀏覽器的歷史記錄;
history使用介紹
按照官方文檔的說明,backbone的history模塊可以實現從pushstate到hashchange到url直接跳轉,這三種方式的優雅降級。為了驗證這一點,我專門做了幾個demo。
先來看demo1:http://liuyunzhuge.github.io/blog/backbone_router/demo1.html
在正式介紹這個demo前,先要了解一點,網頁里的a標簽的href屬性有多種形式:
1)http(s)形式的,如href="/index",href="../index",href="http://www.baidu.com",href="https://www.baidu.com"
2)錨點形式,如href="#container"
3)腳本形式,如href="javascript:;"
4)撥打電話,如href="tel:95555"
5)郵件鏈接,如href="mailto:xxx@yyy.com"
6)其它自定義的形式等
a標簽被點擊時,瀏覽器都會有默認的行為,比如http(s)形式的鏈接會引發網頁的跳轉;錨點形式的鏈接會引發hashchange以及錨點內容的定位。在單頁應用里面,大部分的http(s)形式的鏈接都要采用單頁形式跳轉,但也不是所有的鏈接都要使用單頁形式跳轉。在管理url的時候,要區分出哪些鏈接需要用單頁形式的跳轉。backbone的history模塊可以解決的是url管理問題,但是不會解決如何區分哪些鏈接需要單頁形式跳轉的問題。當然實際上解決后面這個問題也很簡單,后面的部分會詳細說明,這里只是說明這個實現的要點。
接下來在chrome中新開一個選項卡,打開上面的demo1鏈接以及控制台,可看到下面類似的界面:
做一些測試:點擊列表鏈接;點擊詳情鏈接;點擊關於鏈接;點擊瀏覽器倒退;點擊瀏覽器倒退;點擊瀏覽器倒退;點擊瀏覽器前進;點擊瀏覽器前進;點擊瀏覽器前進。觀察瀏覽器地址欄以及控制台的變化(由於沒有考慮在url更改的時候去更改頁面內容,所以只能用控制台的打印的方式來簡單代替一下url變化的頁面內容更改的邏輯)
比如點擊列表鏈接后,頁面狀態應該是這樣的:
在所有的操作中,不難發現整個頁面都是滿足SPA要求的(當然,這個demo不能做刷新操作測試,道理同上)。接下來看看這個demo的一些要點:
首先為了解決前面的提出的區分鏈接是否要做單頁跳轉的問題,在需要單頁跳轉的a元素上增加了一個spa的屬性:
<li><a spa href="/">首頁</a></li> <li><a spa href="/list">列表</a></li> <li><a spa href="/detail/1">詳情</a></li> <li><a spa href="/about">關於</a></li>
demo里面除了頂部的四個鏈接有加spa的標識,還給出了一些普通鏈接,點擊之后會按照默認行為進行交互,用來對比單頁鏈接的行為。觀察這個html結構,也不難發現,盡管這些鏈接都是單頁的,但是它們的href的形式還跟多頁應用里的書寫方式一樣。
然后考慮如何自定義這些單頁鏈接的行為。我的實現是通過事件委托的方式,在document對象上代理這些a元素的點擊事件:
$(document).on('click', 'a[spa]', function (e) { var $a = $(this); var href = $.trim($a.attr('href')); var options = { trigger: $a.data('trigger') !== false,//默認點擊鏈接后都要自動觸發回調 replace: Backbone.history.getFragment(href) == Backbone.history.fragment//如果鏈接地址與當前地址匹配就采用replace的方式 }; Backbone.history.navigate(href, options); e.preventDefault(); });
然后在這個代碼的內部,取消瀏覽器的默認行為,然后很簡單的調用Backbone.history.navigate方法來完成url的跳轉,它會根據瀏覽器對pushState的支持情況以及history模塊初始化的方式,來判斷是用pushstate api更改url還是通過hash來更改url。這個方法的第二參數,是一個選項配置對象,trigger屬性決定是否在url跳轉完畢后,自動觸發相應的回調函數;replace屬性,決定了是否在瀏覽器歷史記錄棧中創建新的條目。考慮到這里是全局定義單頁鏈接的行為,replace屬性是否為true完全根據鏈接的地址是否與當前url匹配決定的,如果匹配那么replace就是true,意味着鏈接是重復點擊,不需要創建新的歷史記錄條目,否則就是false,意味着當前鏈接點擊后需要跳轉到一個新的地址,所以需要創建新的歷史記錄條目;trigger屬性,除非顯示地在a元素上加了data-trigger=false,否則它傳遞到navigate始終是true。
trigger屬性設置為true是比較關鍵的,尤其是當pushstate被使用的時候,navigate內部僅僅只是用pushstate api改變了頁面地址,所以要手工地去觸發相應的回調函數執行。
接着在demo里面還有下面一段簡單的代碼,用到了Backbone.Router模塊,用來完成url規則與回調函數的定義:
var AppRouter = Backbone.Router.extend({ routes: { '': 'index', 'index': 'index', 'list': 'renderList', 'detail/:id': 'renderDetail', '*error': 'renderError' }, index: function () { console.log('首頁action'); }, renderList: function () { console.log('列表action'); }, renderDetail: function (id) { console.log('詳情action, 詳情id為: ' + id); }, renderError: function (error) { console.log('URL錯誤, 錯誤信息: ' + error); } }); var router = new AppRouter();
Router模塊是一個很簡單的模塊,它就是一個url規則與回調函數的映射表,被History模塊的實例所引用,當頁面地址匹配到了Router里面定義的規則時,相應的回調函數就會執行。上面的用法已經比較全了,實際項目中,可以有多個AppRouter這樣的模塊定義,然后各自初始化一個Router實例,這樣就能實現大量的url規則進行拆分管理。
最后通過下面的方式,來初始化啟用了pushstate管理的history模塊:
Backbone.history.start({pushState: true, root: ROOT_BASE + '/demo1.html/'});
(root的作用可以查看backbone官方文檔了解)
以上就是本文要介紹的最重要的內容,它是一種比較簡潔的進行spa的url管理的方法,盡管可能還有一些場景沒有考慮到,但是按照以上思路,即使碰到一些使用不佳的場景,我們也能夠想辦法解決。
回到本部分開頭的內容,來看看backbone是否真能做到url管理的優雅降級。為了驗證這一點,需要模擬另外兩種場景,分別是不支持pushState的場景和不使用pushState&hashchange的場景。
demo2:http://liuyunzhuge.github.io/blog/backbone_router/demo2.html
demo2與demo1的區別僅僅在於最后面初始化history模塊的方式,demo1是這樣的:
Backbone.history.start({pushState: true, root: ROOT_BASE + '/demo1.html/'});
demo2是這樣的:
Backbone.history.start({root: ROOT_BASE + '/demo2.html/'});
demo2的初始化代碼,沒有傳遞pushState屬性,在backbone內部會被認為不需要使用pushstate,現在電腦上安的瀏覽器基本上都支持pushstate,所以在不更改backbone源碼的前提下,只能采取這種形式來模擬一種pushstate不支持,降級到hashchange處理的場景。
好在這個demo里面,其它的代碼形式都沒有改變,最主要是單頁鏈接的地址還是原來的那個形式:
<li><a spa href="/">首頁</a></li> <li><a spa href="/list">列表</a></li> <li><a spa href="/detail/1">詳情</a></li> <li><a spa href="/about">關於</a></li>
接下來在chrome中新開一個選項卡,打開demo2的鏈接,按照demo1的測試操作,做一下測試:點擊列表鏈接;點擊詳情鏈接;點擊關於鏈接;點擊瀏覽器倒退;點擊瀏覽器倒退;點擊瀏覽器倒退;點擊瀏覽器前進;點擊瀏覽器前進;點擊瀏覽器前進。觀察地址欄及控制台的變化。
最后會發現,控制台的打印情況跟demo1是一致的。但是瀏覽器的地址不再是demo1那種形式的地址,比如點擊列表鏈接后,頁面地址變成了這種hash形式:
說明這個場景下的navigate方法,其實是通過改變hash來實現的。然后瀏覽器前進后退的時候也應該是通過hashchange事件來實現的。最后這個demo可以證明,backbone的history模塊,確實可以完美地降級到hashchange來管理url,而且這個變化對於我們的業務代碼來說是透明的。
demo3:http://liuyunzhuge.github.io/blog/backbone_router/demo3.html
這個demo用來模擬連hashchange都無法使用的場景。它與demo1的區別也僅僅在於history模塊初始化的方式,它的是:
Backbone.history.start({hashChange: false, root: ROOT_BASE + '/demo3.html/'});
通過顯示的配置hashChange為false,告訴backbone不使用hashchange管理url。在這個場景下backbone.history.navigate方法,會直接進行url跳轉,而不會用到pushstate以及hash。正是如此,navigate最后並不會觸發url的回調函數,畢竟頁面已經重載了。但是backbone能做到的就是,當頁面初始化的時候,與頁面地址匹配的回調函數還是會被執行,這也就保證了它在這種場景下,url地址與頁面內容的一致性。
不過這個demo3打開后,點擊那些“單頁鏈接”,最后都會報404,畢竟它只是用來模擬這個特殊情況而已。但是從它start方法的源碼來看,backbone確實能做到在頁面初始化的時候執行與它對應的回調。通過這個demo,最后完全證明了backbone在url管理時的兼容程度,確實很全面。實際工作中,可以考慮僅借鑒demo1的方法即可。
最后,還要再補充一下的就是如果是通過demo1那種方式初始化History模塊,那么demo3這個場景應該在絕大部分瀏覽器都不會出現。因為即使在那些不支持hashchange的瀏覽器上,backbone內部也通過定時器加iframe的方式,來模擬hashchange。也就是說如果不手動把hashchange配置為false的話,backbone總會有一個模擬的hashchange作為備用。這樣它就能讓你的應用始終保持SPA的狀態了。要想了解backbone內部管理url的細節,可以僅看backbone源碼部分的router模塊和history模塊,最主要的就是history模塊的start和navigate方法,這兩個方法是url管理初始化和跳轉控制的核心,從中也能學到一些關於location使用的技巧。
小結
這篇文章的目的就是為了介紹spa里面url管理的思路以及backbone來管理url的做法。由於我到目前為止,還沒做過單頁的應用,所以這里面有的方法或者觀點不一定完全正確,希望有經驗的朋友看到之后能幫忙指正,感謝~下一篇介紹如何管理SPA里面的頁面內容,方法正在琢磨中…