什麼是前端路由
所謂的前端路由,擁有這樣一種能力:客戶端瀏覽器可以不依賴服務端,根據不同的URL渲染不同的視圖頁面。
前端路由的存在合理性
在Ajax之劍還未亮出,前端仍處於襁褓之中的時候,路由的工作交給了後端。在進行頁面切換的時候,瀏覽器發送不同的<span style="font-size: 14px;">url</span>
#請求;伺服器接收到瀏覽器的請求時,透過解析不同的<span style="font-size: 14px;">url</span>
去拼接需要的<span style="font-size: 14px;">html</span>
#或模板,然後將結果傳回瀏覽器端進行渲染。
伺服器端路由也是不落俗套的有利亦有弊。它的好處是安全性更高,更嚴格得控制頁面的展現。這在某些場景中是很有用的,譬如下單支付流程,每一步只有在上一步成功執行之後才能抵達。這在伺服器端可以為每個流程新增驗證機制,只有驗證通過才會傳回正確的頁面。那麼前端路由就不能實現每一步的驗證呢?自然不是,姑且相信你的程式碼可以寫的很嚴謹,保證正常情況下流程不會錯,但是另一個不得不面對的事實是:前端是毫無安全性可言的。使用者可以肆意修改程式碼來進入不同的流程,你可能會為此添加不少的處理邏輯。相較之下,當然是後端控制頁面的進入權限更為安全和簡單。
另一方面,後端路由無疑增加了伺服器端的負荷,並且需要<span style="font-size: 14px;">reload</span>
##頁面,用戶體驗其實不佳。 <span style="font-size: 14px;"></span>
這樣,前端路由就有用武之地了。首先,它的出現無疑減輕了伺服器端的壓力。特別是對於一個比較複雜的應用程式來講,或者更確切的說,對於擁有一個複雜路由系統的應用來說,伺服器端需要為每一個不同的url執行一段處理邏輯在高並發的情況下實在有點不堪重負;其次,頁面的切換可以不需要刷新整個頁面了,沒有網路延遲,沒有閃爍刷新,提升了使用者體驗。 <span style="font-size: 14px;"></span>
前端路由實作方式<span style="font-size: 14px;"></span>
既然目標實現,我們需要解決的問題有哪些?我們可以將問題拆的稍微細一點,先制定一個億的小計劃,實現之後再進行下一步:)<span style="font-size: 14px;"></span>
在頁面不刷新的前提下實作<span style="font-size: 14px;"></span>
url<span style="font-size: 14px;"></span>
變化<span style="font-size: 14px;"></span>-
捕捉到<span style="font-size: 14px;"></span>
的變化,以便執行頁面替換邏輯<span style="font-size: 14px;"></span>
url<span style="font-size: 14px;"></span>
<span style="font-size: 14px;"></span>#如何實現更新<span style="font-size: 14px;"></span>
url <span style="font-size: 14px;"></span>
且頁面不刷新<span style="font-size: 14px;"></span>如前面所說,前端路由相較於後端路由的一個特點就是頁面在不完全刷新的情況下進行視圖的切換。頁面<span style="font-size: 14px;"></span>
url<span style="font-size: 14px;"></span>
<span style="font-size: 14px;"></span>試想將瀏覽器網址列當做一個輸入框,我們需要實現的就是改變輸入框的<span style="font-size: 14px;"></span>
value<span style="font-size: 14px;"></span>但是不觸發請求頁面的操作,這樣就不會重新載入新頁面。倘若輸入框的值的變化和發送請求是一個原子操作,我們也就束手無策了。慶幸的是,只有當我們敲擊了回車之後,請求才會被發送出去(這是顯而易見的吧)。因此這就為我們修改網址列的值而不觸發頁面請求刷新創造了條件。 BOM是否有提供修改瀏覽器網址列<span style="font-size: 14px;"></span>
url<span style="font-size: 14px;"></span>
這裡,有兩種滿足需求的方式。一是利用<span style="font-size: 14px;">url</span>
中的<span style="font-size: 14px;">#hash</span>
欄位;二是使用HTML5提供的history API。
<span style="font-size: 14px;">hash</span>
方式
了解<span style="font-size: 14px;">http</span>
協定就會知道,<span style="font-size: 14px;">url</span>
的組成部分有很多,如協定、主機名稱、資源路徑、查詢欄位等等等,其中包含一個稱為片段的部分,以<span style="font-size: 14px;"></span>
#為標識。
例如: <span style="font-size: 14px;">http://www.gmail.com/text/#123</span>
, <span style="font-size: 14px;">123</span>
便是<span style="font-size: 14px;">url</span>
<span style="font-size: 14px;"></span><span style="font-size: 14px;">#hash</span>
部分。
打開控制台,輸入<span style="font-size: 14px;">location.hash</span>
,你可以得到目前<span style="font-size: 14px;">#url</span>
的<span style="font-size: 14px;">hash</span>
#部分(如果目前<span style="font-size: 14px;">url</span>
不存在<span style="font-size: 14px;">hash</span>
則傳回空字串)。接下來,輸入<span style="font-size: 14px;">location.hash = '123'</span>
#,會發現瀏覽器網址列的<span style="font-size: 14px;">url</span>
變了,末尾增加了<span style="font-size: 14px;">#123</span>
字段,並且,頁面沒有被重新刷新。很顯然,這很符合我們的要求。
history API
HTML5引入了一個<span style="font-size: 14px;">history</span>
對象,包含了一套存取瀏覽器歷史的API,可以透過<span style="font-size: 14px;">window.history</span>
存取到它。
這裡我們看上了它的兩個API方法:<span style="font-size: 14px;">#pushState</span>
和 <span style="font-size: 14px;">replaceState</span>
。
<span style="font-size: 14px;">history.replaceState(dataObj, title, url);<br/>history.pushState(dataObj, title, url);<br/></span>
若上所示,它們接收完全相同的參數,都是對瀏覽器的歷史堆疊進行操作,將傳遞的url和相關資料壓棧,並將瀏覽器網址列的<span style="font-size: 14px;">url</span>
替換為傳入的<span style="font-size: 14px;">#url</span>
##且不刷新頁面(正中下懷!)。 <span style="font-size: 14px;"></span>
By the way,不同的地方是<span style="font-size: 14px;"></span>
#pushState<span style="font-size: 14px;"></span>
將指定的<span style="font-size: 14px;"></span>url<span style="font-size: 14px;"></span>
直接壓入歷史記錄棧頂,而<span style="font-size: 14px;"></span>#replaceState<span style="font-size: 14px;"></span>
是將目前歷史記錄堆疊頂替換成傳入的數據。 <span style="font-size: 14px;"></span>
這兩種方式都可以幫我們滿足題設條件。採用哪一種方式除了主觀喜好之外,還得依照客觀事實:低版本的瀏覽器對於history API的兼容性不好,例如遇到了IE8,擺在眼前的道路似乎就別無選擇了。 <span style="font-size: 14px;"></span>
如何追蹤<span style="font-size: 14px;"></span>url<span style="font-size: 14px;"></span>
#變化<span style="font-size: 14px;"></span>
在瀏覽器端,追蹤表單屬性的變化一般都採用事件監聽機制,追蹤<span style="font-size: 14px;"></span>url<span style="font-size: 14px;"></span>
的變化也不落俗套。 <span style="font-size: 14px;"></span>
对于<span style="font-size: 14px;">hash</span>
方式的前端路由,通常可以监听 <span style="font-size: 14px;">hashchange</span>
事件,在事件回调中处理相应的页面视图展示等逻辑。
此外,HTML5提供的 <span style="font-size: 14px;">popstate</span>
事件也会在<span style="font-size: 14px;">url</span>
的<span style="font-size: 14px;">hash</span>
发生改变时触发。也就是说如果可以忽略低版本浏览器,我们使用<span style="font-size: 14px;">hash</span>
方式路由时也可以采用监听这个事件进行回调处理。
那么,如果是采用history API的形式呢?根据MDN的描述:
调用
<span style="font-size: 14px;">history.pushState()</span>
或者<span style="font-size: 14px;">history.replaceState()</span>
不会触发<span style="font-size: 14px;">popstate</span>
事件。<span style="font-size: 14px;">popstate</span>
事件只会在浏览器某些行为下触发, 比如点击后退按钮(或者在JavaScript中调用<span style="font-size: 14px;">history.back()</span>
方法)。
这也就是说,我们在使用history API改变浏览器的<span style="font-size: 14px;">url</span>
时,仍需要额外的步骤去触发 <span style="font-size: 14px;">popstate</span>
事件,例如调用 <span style="font-size: 14px;">history.back()</span>
会 <span style="font-size: 14px;">history.forward()</span>
等方法。
从兼容性上来讲,前面有提及<span style="font-size: 14px;">hash</span>
的方式兼容性更好。然而,对于低版本的浏览器,例如IE6等等,不支持 <span style="font-size: 14px;">hashchange</span>
事件。这个时候我们只能通过 <span style="font-size: 14px;">setInterval</span>
设置心跳的方式去模拟 <span style="font-size: 14px;">hashchange</span>
。
<span style="font-size: 14px;">var oldHash = location.hash;var oldURL = location.href;<br/><br/>setInterval(function() { var newHash = location.hash; var newURL = location.href; if (newHash !== oldHash && typeof window.onhashchange === 'function') { // 执行onhashchange回调<br/> window.onhashchange({ 'type': 'hashchange', 'oldURL': oldURL, 'newURL': newURL<br/> });<br/><br/> oldHash = newHash;<br/> oldURL = newURL;<br/> }<br/>}, 100);<br/></span>
一个简单实现
这里,给出一个很简单的实现:
<span style="font-size: 14px;">router.js</span>
<span style="font-size: 14px;">function FrontRouter() { this.routes = {};<br/> window.addEventListener('load', this.resolve.bind(this), false);<br/> window.addEventListener('hashchange', this.resolve.bind(this), false);<br/>}<br/><br/>FrontRouter.prototype.route = function(path, callback) { this.routes[path] = callback || function() {};<br/>};<br/><br/>FrontRouter.prototype.resolve = function() { this.curHash = location.hash.slice(1) || '/'; typeof this.routes[this.curHash] === 'function' && this.routes[this.curHash]();<br/>};<br/></span>
<span style="font-size: 14px;">index.html</span>
<span style="font-size: 14px;"><ul> <li><a href='#blue'></a></li> <li><a href='#yellow'></a></li> <li><a href='#red'></a></li></ul><br/></span>
<span style="font-size: 14px;">index.js</span>
<span style="font-size: 14px;">var router = new FrontRouter();<br/><br/>router.route('blue', function() {<br/> document.body.style.backgroundColor = 'blue';<br/>});<br/><br/>router.route('yellow', function() {<br/> document.body.style.backgroundColor = 'yellow';<br/>});<br/><br/>router.route('red', function() {<br/> document.body.style.backgroundColor = 'red';<br/>});<br/></span>
一点总结
应用场景
前端路由大部分的应用场景,就是我们现在熟知的单页应用SPA。
不存在纯前端路由
我们此前所描述的前端路由,建立在已经打开了一个初始页面基础之上,然后在这个页面之内进行页面替换。然而,我们如何进入这个初始页面?仅靠前端路由肯定是力所不及。我们至少要向后端发送一次http请求,接收所需要加载的页面不是吗?
所以,我们并不能抛弃后端路由部分。这也意味着,我们需要和后端确认各自的分工,哪些url归前端解析,哪些归后台解析。