SPA 路由三部曲之核心原理


為了配合單頁面 Web 應用快速發展的節奏,近幾年,各類前端組件化技術棧層出不窮。通過不斷的版本迭代 React、Vue 脫穎而出,成為當下最受歡迎的兩大技術棧。

僅 7 個月的時間,兩個技術棧的下載量就突破了百萬,React 甚至突破了千萬。不管是現下流行的 React、Vue,還是紅極一時的 Angular、Ember,只要是單頁面 Web 應用,都離不開前端路由的配合。如果把單頁面 Web 應用比作一間房,每個頁面對應房子中的各個房間,那么路由就是房間的門,不管房間裝飾的有多漂亮,沒有門,也無法展示在用戶眼前,路由在單頁面 Web 應用的地位也就不言而喻了。

為了能更詳細的介紹前端路由,小編將從三個層面,由淺入深,一步一步的帶領大家探索前端路由的實現原理。首先通過《SPA 路由三部曲之核心原理》了解前端路由的核心知識,緊接着《SPA 路由三部曲之 MyVueRouter 實踐》將帶領大家實現屬於自己的 vue-router,最后《SPA 路由三部曲之 VueRouter 源碼解析》將挑戰自我,深度解析 vue-router 源碼。《SPA 路由三部曲之核心原理》將從端路由的前世今生、核心原理解析、vue-router 與 react-router 應用對比三部分對前端路由進行初步了解。

前端路由前世今生

前端路由發展到今天,經歷了后端路由、前后端路由過渡、前端路由的過程,如果你對前端路由的理解還是懵懵懂懂,那有必要了解一下它的發展過程。

后端路由

路由這個概念最先是在后端出現的, Web 開發還在「刀耕火種」年代時,一直是后端路由占據主導地位,頁面渲染完全依賴服務器。

在最開始的時候,HTML、CSS、JavaScript 的文件以及數據載體 json(xml) 等文件都是放到后端服務器目錄下的,並且這些文件彼此是沒有聯系的,想要改變網站的布局,可能會改上百個 HTML,繁瑣且毫無技術含量。后來聰明的工程師就將相同的 HTML 整理成模板,進行復用,成功減少了前端的工作量。前端工程師開始用模板語言代替手寫 HTML,后端服務器目錄的文件也變成了不同的模板文件。

這個時期,不管 Web 后端是什么語言的框架,都會有一個專門開辟出來的路由模塊或者路由區域,用來匹配用戶給出的 URL 地址,以及一些表單提交、頁面請求地址。用戶進行頁面切換時,瀏覽器發送不同的 URL 請求,服務器接收到瀏覽器的請求時,通過解析不同的 URL 地址進行后端路由匹配,將模板拼接好后將之返回給前端完整的 HTML,瀏覽器拿到這個 HTML 文件后直接解析展示了,也就是所謂的服務端渲染。

服務端渲染

服務端渲染頁面,后端有完整的 HTML 頁面,爬蟲更容易獲取信息,有利於 SEO 優化。對於客戶端的資源占用更少,尤其是移動端,可以更省電。

過渡

以后端路由為基礎,開發的 Web 應用,都會存在一個弊端。每跳轉到不同的 URL,都是重新訪問服務端,服務器拼接形成完整的 HTML,返回到瀏覽器,瀏覽器進行頁面渲染。甚至瀏覽器的前進、后退鍵都會重新訪問服務器,沒有合理地利用緩存。

隨着前端頁面復雜性越來越高,功能越來越完善,后端服務器目錄下的代碼文件會越來越多,耦合性也越來越嚴重。不僅加大服務器的壓力,也不利於良好的用戶體驗,代碼維護。受限於以 JavaScript 為代表的前端技術尚未崛起,這個痛點成了程序員的最大難題。

直到 1998 年,微軟的 Outloook Web App 團隊提出 Ajax 的基本概念(XMLHttpRequest 的前身),相信大家對這個技術已經非常熟悉了,瀏覽器實現異步加載的一種技術方案,並在 IE5 通過 ActiveX 來實現了這項技術。有了 Ajax 后,頁面操作就不用每次都刷新頁面,體驗帶來了極大的提升。

2005 年 Google Map 的發布讓 Ajax 這項技術發揚光大,向人們展示了它真正的魅力,讓其不僅僅局限於簡單的數據和頁面交互,也為后來異步交互體驗方式的繁榮發展奠定了基礎。2008 年,Google V8 引擎發布,JavaScript 隨之崛起,前端工程師開始借鑒后端模板思想,單頁面應用就此誕生。2009 年,Google 發布 Angularjs 將 MVVM 及單頁面應用發揚光大,由衷的佩服 Google 的強大。

單頁應用不僅在頁面交互是無刷新的,連頁面跳轉都是無刷新的,為了配合實現單頁面應用跳轉,前端路由孕育而生。

前端路由

前端路由相較於后端路由的一個特點就是頁面在不完全刷新的情況下進行視圖的切換。頁面 URL 變了,但是並沒有重新加載,讓用戶體驗更接近原生 app。

前端路由的興起,使得頁面渲染由服務器渲染變成了前端渲染。為什么這么說呢!請求一個 URL 地址時,服務器不需要拼接模板,只需返回一個 HTML 即可,一般瀏覽器拿到的 HTML 是這樣的:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Demo</title>
  <link href="app.css" rel="stylesheet"/>
</head>
<body>
  <div id="app"></div>
  <script type="text/javascript" src="app.js"></script>
</body>
</html>

這里空盪盪的只有一個 <div id="app"></div>,以及一系列的 js 文件,所以說這個 HTML 是不完整的。我們看到的頁面是通過這一系列的 js 渲染出來的,也就是前端渲染。前端渲染通過客戶端的算力來解決頁面的構建,很大程度上緩解了服務端的壓力。

客戶端渲染

單頁面開發是趨勢,但也不能避重就輕,忽略前端渲染的缺點。由於服務器沒有保留完整的 HTML,通過 js 進行動態 DOM 拼接,需要耗費額外的時間,不如服務端渲染速度快,也不利於 SEO 優化。所以說,實際開發中,不能盲目選擇渲染方式,一定要基於業務場景。對於沒有復雜交互,SEO 要求嚴格的網站,服務器渲染也是正確的選擇。

核心原理解析

路由描述了 URL 與 UI 之間的映射關系,這種映射是單向的,即 URL 變化引起 UI 更新(無需刷新頁面)。前端路由最主要的展示方式有 2 種:

  • 帶有 hash 的前端路由:地址欄 URL 中有 #,即 hash 值,不好看,但兼容性高。
  • 不帶 hash 的前端路由:地址欄 URL 中沒有 #,好看,但部分瀏覽器不支持,還需要后端服務器支持。

在 vue-router 和 react-router 中,這兩種展示形式,被定義成兩種模式,即 Hash 模式與 History 模式。前端路由實現原理很簡單,本質上就是檢測 URL 的變化,截獲 URL 地址,通過解析、匹配路由規則實現 UI 更新。現在就跟着小編一起來揭開它神秘的面紗吧!

Hash

一個完整的 URL 包括:協議、域名、端口、虛擬目錄、文件名、參數、錨。

URL 組成

hash 值指的是 URL 地址中的錨部分,也就是 # 后面的部分。hash 也稱作錨點,是用來做頁面定位的,與 hash 值對應的 DOM id 顯示在可視區內。在 HTML5 的 history 新特性出現前,基本都是使用監聽 hash 值來實現前端路由的。hash 值更新有以下幾個特點:

  • hash 值是網頁的標志位,HTTP 請求不包含錨部分,對后端無影響
  • 因為 HTTP 請求不包含錨部分,所以 hash 值改變時,不觸發網頁重載
  • 改變 hash 值會改變瀏覽器的歷史記錄
  • 改變 hash 值會觸發 window.onhashchange() 事件

而改變 hash 值的方式有 3 種:

  • a 標簽使錨點值變化,例: <a href='#/home'></a>
  • 通過設置 window.location.hash 的值
  • 瀏覽器前進鍵(history.forword())、后退鍵(history.back())

綜上所述,這 3 種改變 hash 值的方式,並不會導致瀏覽器向服務器發送請求,瀏覽器不發出請求,也就不會刷新頁面。hash 值改變,觸發全局 window 對象上的 hashchange 事件。所以 hash 模式路由就是利用 hashchange 事件監聽 URL 的變化,從而進行 DOM 操作來模擬頁面跳轉。
hash 流程圖

History

在講解 History 之前,大家先思考一個問題,點擊瀏覽器左上角的回退按鈕為什么能回到之前的瀏覽記錄,點擊前進按鈕就能回到回退之前的瀏覽記錄?這是因為瀏覽器有一個類似棧的歷史記錄,遵循先進后出的規則。URL 的每次改變,包括 hash 值的變化都會在瀏覽器中形成一條歷史記錄。window 對象通過 history 對象提供對覽器歷史記錄的訪問能力。

  • history.length
    出於安全考慮,History 對象不允許未授權代碼訪問歷史記錄中其它頁面的 URLs,但可以通過 history.length 訪問歷史記錄對象的長度。
  • history.back()
    回退到上一個歷史記錄,同瀏覽器后退鍵
  • history.forward()
    前進到下一個歷史記錄,同瀏覽器前進鍵
  • history.go(n)
    跳轉到相應的訪問記錄;若 n > 0,則前進,若 n < 0,則后退,若 n = 0,則刷新當前頁面

為了配合單頁面的發展,HTML5 對 History API 新增的兩個方法:pushState()、replaceState(),均具有操縱瀏覽器歷史記錄的能力。

history.pushState(state, title, URL)

pushState 共接收 3 個參數:

  • state:用於存儲該 URL 對應的狀態對象,可以通過 history.state 獲取
  • title:標題,目前瀏覽器並不支持
  • URL:定義新的歷史 URL 記錄,需要注意,新的 URL 必須與當前 URL 同源,不能跨域

pushState 函數會向瀏覽器的歷史記錄中添加一條,history.length 的值會 +1,當前瀏覽器的 URL 變成了新的 URL。需要注意的是:僅僅將瀏覽器的 URL 變成了新的 URL,頁面不會加載、刷新。簡單看個例子:

通過 history.pushState({ tag: "cart" }, "", "cart.html"),將 /home.html 變成 /cart.html 時,只有 URL 發生了改變,cart.html 頁面並沒有加載,甚至瀏覽器都不會去檢測該路徑是不是存在。這也就是證明了,pushState 在不刷新頁面的情況下修改瀏覽器 URL 鏈接,單頁面路由的實現也就是利用了這一個特性。

細心地童鞋應該發現了,通過 pushState 設置新的 URL 的方法與通過 window.location='#cart' 設置 hash 值改變 URL 的方法有相似之處:URL 都發生了改變,在當前文檔內都創建並激活了新的歷史記錄條目,但頁面均沒有重新渲染,瀏覽器沒有發起請求。那前者的優勢又是什么呢?

  • 新的 URL 可以是任意同源的 URL,而 window.location,只能通過改變 hash 值才能保證留在當前 document 中,瀏覽器不發起請求
  • 新的 URL 可以是當前 URL,不改變,就可以創建一條新的歷史記錄項,而 window.location 必須設置不同的 hash 值,才能創建。假如當前URL為 /home.html#foo,使用 window.location 設置 hash 時,hash
    值必須不能是 #foo,才能創建新的歷史記錄
  • 可以通過 state 參數在新的歷史記錄項中添加任何數據,而通過 window.location 改變 hash 的方式,只能將相關的數據轉成一個很短的字符串,以 query 的形式放到 hash 值后面
  • 雖然 title 參數現在還不能被所有的瀏覽器支持,前端發展這么快,誰能說的准之后發生的事情呢!

history.replaceState(state, title, URL)

replaceState 的使用與 pushState 非常相似,都是改變當前的 URL,頁面不刷新。區別在於 replaceState 是修改了當前的歷史記錄項而不是新建一個,history.length 的值保持不變。

從上面的動畫,我們就可以知道,通過 history.replaceState({ tag: "cart" }, "", "cart.html") 改變 URL 之前,history 的歷史記錄為 /classify.html/home.html,URL 改變之后,點擊瀏覽器后退鍵,直接回到了 /classify.html,跳過了 /home.html。也就證明了 replaceState 將歷史記錄中的 /home.html 修改為 /cart.html,而不是新建 /cart.html

window.onpopstate()

通過 a 標簽或者 window.location 進行頁面跳轉時,都會觸發 window.onload 事件,頁面完成渲染。點擊瀏覽器的后退鍵或前進鍵,根據瀏覽器的不同機制,也會重新加載(Chrome 瀏覽器),或保留之前的頁面(Safari 瀏覽器)。而對於通過 history.pushState() 或 history.replaceState() 改變的歷史記錄,點擊瀏覽器的后退鍵或前進鍵頁面是沒有反應的,那該如何控制頁面渲染呢?為了配合 history.pushState() 或 history.replaceState(),HTML5 還新增了一個事件,用於監聽 URL 歷史記錄改變:window.onpopstate()。

官方對於 window.onpopstate() 事件的描述是這樣的:

每當處於激活狀態的歷史記錄條目發生變化時,popstate 事件就會在對應 window 對象上觸發。 如果當前處於激活狀態的歷史記錄條目是由 history.pushState() 方法創建,或者由 history.replaceState() 方法修改過的, 則 popstate 事件對象的 state 屬性包含了這個歷史記錄條目的 state 對象的一個拷貝。調用 history.pushState() 或者 history.replaceState() 不會觸發 popstate 事件。popstate 事件只會在瀏覽器某些行為下觸發, 比如點擊后退、前進按鈕(或者在JavaScript 中調用 history.back()、history.forward()、history.go()方法),此外,a 標簽的錨點也會觸發該事件。

第一次讀到這段話的時候似懂非懂,思考了很久,也做了很多的例子,發現其中的坑很多,這些坑主要是因為每個瀏覽器機制不同。官方文檔對 window.onpopstate() 的描述很少,也有很多不明確的地方,根據自己的測試,來拆解一下官網描述,如果有不對的,還希望大家指出。

1.每當處於激活狀態的歷史記錄條目發生變化時,popstate 事件就會在對應 window 對象上觸發。

對這句話的理解是,在瀏覽器中輸入一個 URL ,使其處於激活狀態,不管通過哪種方式,只要 URL 改變,popstate 就會觸發。但實際情況卻是:只有通過 pushState 或 replaceState 改變的 URL,在點擊瀏覽器后退鍵的時候才會觸發,如果是通過 a 標簽或 window.location 實現 URL 改變(不是改變錨點)頁面跳轉,在點擊瀏覽器回退鍵的時候,並不會觸發。對這種情況,我有兩個猜測:

  • popstate 事件是異步函數。由於通過 a 標簽或 window.location 實現 URL 改變時,當前頁面卸載,新的頁面加載。由於 popstate 事件是異步的,在頁面卸載之前並沒來得及加載。
  • 只有觸發新增的 pushState 與 replaceState 改變的歷史記錄條目,才會觸發 popstate 事件,畢竟 popstate 事件的出現是為了配合 pushState 與 replaceState。
    查閱了很多資料,這兩個猜測沒有得到證實,但有一點可以肯定,想要監聽到 popstate 事件,必須是使用 pushState 與 replaceState 改變的歷史記錄。

2.調用 history.pushState() 或者 history.replaceState() 不會觸發 popstate 事件,popstate 事件只會瀏覽器的某些行為下觸發。

由於各個瀏覽器的機制不同,測試結果也是不同的。我們先在 Chrome 瀏覽器下做個測試:
home.html

<div>
  <h3>home html</h3>
  <div id="btn" class="btn">跳轉至 cart.html</div>
  <a href="classify.html"> a 標簽跳轉至 classify.html</a>
</div>
<script>
  document.getElementById('btn').addEventListener('click', function(){
       history.replaceState({ tag: "cart" }, "", "cart.html")
   }, false); 
   window.addEventListener('popstate', ()=>{
      console.log('popstate home 跳轉')
   })
</script>

我們進行這樣的操作:當前 URL 為 /home.html,通過 history.pushState({ tag: "cart" }, "", "cart.html") 將當前 URL 變成了 /cart.html。這個過程中,home.html 中的 popstate 事件確實沒有觸發。此時點擊瀏覽器后退鍵,URL 變回了/home.htmlhome.html 中的 popstate 事件觸發了。

那如果我們跳出 /home.html 的 document 呢?通過 history.pushState({ tag: "cart" }, "", "cart.html") 將當前 URL 變成了 /cart.html 后,點擊 a 標簽將 URL 變為 /classify.html

執行到這里,我們需要明確一點:a 標簽改變 URL,瀏覽器會重新發起請求,頁面發生了跳轉,window 對象也發生了改變。popstate 官方文檔第一句指出: popstate 事件是在對應 window 對象上觸發。此時,我們點擊瀏覽器后退鍵,URL 變成 /cart.html,執行 /cart.html 中的 load 事件,頁面加載。再次點擊瀏覽器后退鍵,URL 變為 /home.html/cart.html 中的 popstate 事件觸發,頁面未渲染。

popstate 事件雖然觸發了,但是是 cart.html 頁面中定義的 popstate 事件,並不是 home.html 的事件。並且同樣的瀏覽器回退鍵操作,在 Safari 瀏覽器的展示是這樣的:

在瀏覽器回退時,Safari 瀏覽器與 Chrome 瀏覽器對於頁面的加載出現了差異。classify.html 回退到 cart.html ,URL 變成了 /cart.html,但觸發了 home.html 中的 popstate 事件,繼續回退,URL 變成了 /home.html, 依然觸發了 home.html 中 popstate 事件。

Chrome 瀏覽器與 Safari 瀏覽器差異的產生與瀏覽器對 popstate 事件處理有關系。至於瀏覽器內部是怎樣處理的,小編也沒有研究清楚。雖然 Chrome 瀏覽器與 Safari 瀏覽器對於 popstate 事件的處理方式不一樣,但是 URL 的回退路徑是一致的,完全符合歷史記錄后進先出的規則。

在實際開發中,這種情況也是存在的:URL 由 /home.html/cart.html 的改變,就類似單頁面開發中的跳轉。若此時在 cart.html 中,需要使用 pushState 跳出單頁面,進入登錄頁,用戶在登錄頁點擊瀏覽器回退,或移動端手勢返回。上述情況就會出現,Chrome 瀏覽器與 Safari 瀏覽器渲染頁面不一致。

popstate 官網描述是“popstate 事件會在對應 window 對象上觸發”,注意是對應 window 對象,這個概念就比較模糊了,指的是觸發 pushState 的 window 對象,還是 pushState 新定義的 window 對象。根據我們上述的測試,都有可能觸發 popstate 事件。所以童鞋們,在遇到上面情況時,一定不要忘記在相關的兩個頁面中都要做 popstate 監聽處理。

3.a 標簽的錨點也可以觸發 popstate 事件的方法

與 pushState 和 replaceState 不同,a 標簽錨點的變化會立即觸發 popstate 事件。這里我們擴展一下思路,a 標簽做的事情就是改變了 hash 值,那通過 window.location 改變 hash 值是不是也是能立即觸發 popstate。答案是肯定的,也會立即觸發 popstate。

通過 hash 小節的了解,hash 值的改變會觸發 hashchange 事件,所以,hash 值的改變會同時觸發 popstate 事件與 hashchange 事件,但如果改變的 hash 值與當前 hash 值一樣的話,hashchange 事件不觸發,popstate 事件觸發。之前我們說過,window.location 設置的 hash 值必須與當前 hash 值不一樣才能新建一條歷史記錄,而 pushState 卻可以。

結合上述,在瀏覽器支持 pushState 的情況下,hash 模式路由也可以使用 pushState 、replaceState 和 popstate 實現。pushstate 改變 hash 值,進行跳轉,popstate 監聽 hash 值的變化。小小的劇透,vue-router 中不管是 hash 模式,還是 history 模式,只要瀏覽器支持 history 的新特性,使用的都是 history 的新特性進行跳轉。

前端路由應用

其實 history 和 hash 都是瀏覽器自有的特性,單頁面路由只是利用了這些特性。在不跳出當前 document 的情況下,除了 history 自身的兼容性之外,各個瀏覽器都不會存在差異,而單頁面開發就是在一個 document 中完成所有的交互,這兩者的完美結合,將前端開發提升到了一個新的高度。

vue-router 和 react-router 是現在最流行的路由狀態管理工具。兩者實現原理雖然是一致的,但由於所依賴的技術棧不同,使用方式也略有不同。在 react 技術棧開發時,大部分的童鞋還是喜歡使用 react-router-dom ,它基於 react-router,加入了在瀏覽器運行環境下的一些功能。

注入方式

1. vue-router

vue-router 可以在 vue 項目中全局使用,vue.use() 功不可沒。通過 vue.use(),向 VueRouter 對象注入了 Vue 實例,也就是根組件。根組件將 VueRouter 實例一層一層的向下傳遞,讓每個渲染的子組件擁有路由功能。

import VueRouter from 'vue-router'
const routes = [
    { path: '/',name: 'home',component: Home,meta:{title:'首頁'} }
]
const router = new myRouter({
    mode:'history',
    routes
})
Vue.use(VueRouter)

2. react-router-dom

react-router 的注入方式是在組件樹頂層放一個 Router 組件,然后在組件樹中散落着很多 Route 組件,頂層的 Router 組件負責分析監聽 URL 的變化,在其下面的 Route 組件渲染對應的組件。在完整的單頁面項目中,使用 Router 組件將根組件包裹,就能完成保證正常的路由跳轉。

import { BrowserRouter as Router, Route } from 'react-router-dom';
class App extends Component {
    render() {
        return (
            <Router>
                <Route path='/' exact component={ Home }></Route>
            </Router>
        )
    }
}

基礎組件

1. vue-router 提供的組件主要有 <outer-link/> 和 <router-view/>

  • <router-link/> 可以操作 DOM 直接進行跳轉,定義點擊后導航到哪個路徑下;對應的組件內容渲染到 <router-view/> 中。

2. react-router-dom 常用到的是 <BrowserRouter/>、<HashRouter/>、<Route/>、<Link/>、<Switch/>

  • <BrowserRouter/>、<HashRouter/> 組件看名字就知道,用於區分路由模式,並且保證 React 項目具有頁面跳轉能力。

  • <Link /> 組件與 vue-router 中的 <router-link/> 組件類似,定義點擊后的目標導航路徑,對應的組件內容通過 <Route /> 進行渲染。

  • <Switch/> 用來將 react-router 由包容性路由轉換為排他性路由,每次只要匹配成功就不會繼續向下匹配。vue-router 屬於排他性路由。

路由模式

1. vue-router 主要分為 hash 和 history 兩種模式。在 new VueRouter() 時,通過配置路由選項 mode 實現。

  • Hash 模式:地址欄 URL 中有 #。vue-router 優先判斷瀏覽器是否支持 pushState,若支持,則通過 pushState 改變 hash 值,進行目標路由匹配,渲染組件,popstate 監聽瀏覽器操作,完成導航功能,若不支持,使用 location.hash 設置 hash 值,hashchange 監聽 URL 變化完成路由導航。

  • History 模式:地址欄 URL 中沒有 #。與 Hash 模式實現導航的思路是一樣的。不同的是,vue-router 提供了 fallback 配置,當瀏覽器不支持 history.pushState 控制路由是否應該回退到 hash 模式。默認值為 true。

    網上資料對 Hash 路由模式的原理分析大都是通過 location.hash 結合 hashchange 實現,與上述描述的 hash 路由模式的實現方式不同,這也是小編最近閱讀 vue-router 源碼發現的,鼓勵小伙伴們讀一下,肯定會收獲滿滿!

2. react-router-dom 常用的 2 種模式是 browserHistory、hashHistory,直接用 <BrowserRouter> 或 <HashHistory> 將根組件(通常是 <App> )包裹起來就能實現。

  • react-router 的實現依賴 history.js,history.js 是 JavaScript 庫。<BrowserRouter> 、 <HashHistory> 分別基於 history.js 的 BrowserHistory 類、HashHistory 類實現。

  • BrowserHistory 類通過 pushState、replaceState 和 popstate 實現,但並沒有類似 vue-router 的兼容處理。HashHistory 類則是直接通過 location.hash、location.replace 和 hashchange 實現,沒有優先使用 history 新特性的處理。

嵌套路由與子路由

1. vue-router 嵌套路由

在 new VueRouter() 配置路由表時,通過定義 Children 實現嵌套路由,無論第幾層的路由組件,都會被渲染到父組件 <router-view/> 標識的地方。

router.js

const router = new Router({
    mode:'history',
    routes: [{
        path: '/nest',
        name: 'nest',
        component: Nest,
        children:[{
            path:'first',
            name:'first',
            component:NestFirst
        }]
    }]
})

nest.vue

<div class="nest">
    一級路由 <router-view></router-view>
</div>

first.vue

<div class="nest">
    二級路由 <router-view></router-view>
</div>

/nest 下設置了二級路由 /first,二級對應的組件渲染在一級路由匹配的組件 <router-view/> 標識的地方。在配置子路由時,path 只需要是當前路徑即可。

2. react-router 子路由

react-router 根組件會被渲染到 <Router/> 指定的位置,子路由則會作為子組件,由父組件指定該對象的渲染位置。如果想要實現上述 vue-router 嵌套的效果,需要這樣設置:

route.js

const Route = () => (
    <HashRouter>
        <Switch>
            <Route path="/nest" component={Nest}/>
        </Switch>
    </HashRouter>
);

nest.js

export default class Nest extends Component {
    render() {
        return (
            <div className="nest">
                一級路由
                <Switch>
                    <Route path="/nest/first" component={NestFirst}/>
                </Switch>
            </div>
        )
    }
}

first.js

export default class NestFirst extends Component {
    render() {
        return (
            <div className="nest">
                二級路由
                <Switch>
                    <Route exact path="/nest/first/second" component={NestSecond}/>
                </Switch>
            </div>
        )
    }
}

其中,/nest 為一級路由,/fitst 二級路由匹配的組件,作為一級路由的子組件。react-router 定義子路由 path 時,需要寫完整的路徑,即父路由的路徑要完整。

路由守衛

1. vue-router 導航守衛分為全局守衛、路由獨享守衛、組件內的守衛三種。主要用來通過跳轉或取消的方式守衛導航。

a. 全局守衛

  • beforeEach — 全局前置鈎子(每個路由調用前都會觸發,根據 from 和 to 來判斷是哪個路由觸發)
  • beforeResolve — 全局解析鈎子(和 router.beforeEach 類似,區別是在導航被確認之前,同時在所有組件內守衛和異步路由組件被解析之后,解析守衛就被調用)
  • afterEach — 全局后置鈎子

b. 路由獨享守衛

  • 路由配置上可以直接定義 beforeEnter 守衛。

c. 組件內守衛

  • beforeRouteEnter — 在渲染該組件的對應路由被 confirm 前調用,不能獲取組件實例 this,因為當守衛執行前,組件實例還沒被創建。
  • beforeRouteUpdate — 當前路由改變,但是該組件被復用時調用
  • beforeRouteLeave — 導航離開該組件的對應路由時調用

2. react-router 4.0 版本之前,提供了 onEnter 和 onLeave 鈎子,實現類似 vue-router 導航守衛的功能,但 4.0 版本后取消了該方法。

路由信息

1. vue-router 中 $router、$route 對象

vue-router 在注冊時,為每個 vue 實例注入了 $router、$route 對象。$router 為 router 實例信息,利用 push 和 replace 方法實現路由跳轉,$route 提供當前激活的路由信息。

import router from './router'
export default new Vue({
    el: '#app',
    router,
    render: h => h(App),
})

2. react-router 中 history、location 對象

在每個由 <Route/> 包裹的組件中提供了 history、location 對象。利用 this.props.history 的 push、replace 方法實現路由導航,this.props.location 獲取當前激活的路由信息。

const BasicRoute = () => (
    <div>
        <HeaderNav></HeaderNav>
        <HashRouter>
            <Switch>
                <Route exact path="/" component={Home}/>
            </Switch>
        </HashRouter>
    </div>
);

如果想要獲得 history、location 一定是 <Route /> 包裹的組件。所以在 <HeaderNav/> 中是無法獲取這兩個對象的,而 <Home/> 組件是可以的。

vue-router 是全局配置方式,react-router 是全局組件方式,但兩者呈現給開發者的功能實際上是大同小異的。當然,vue-router 與 react-router 在使用上的差異不僅僅是小編說的這些。說到底,不管用什么樣的方式實現,前端路由的實現原理都是不會變的。

總結

前端路由的初步體驗馬上就要結束了,在決定深入研究前端路由之前,小編自信滿滿,感覺應該不會花費很大的精力與時間,可事實是,涉及到的知識盲區越來越多,信心在逐漸瓦解。好在結局不錯,收獲了很多,也希望《SPA 路由三部曲之核心原理》這篇文章能讓大家有所收獲,哪怕只是一個知識點。

小編已經在爭分奪秒的准備《SPA 路由三部曲之 MyVueRouter 實踐》、《SPA 路由三部曲之 VueRouter 源碼解析》過程中了,小編相信是不會讓你失望的,請充滿期待吧!

PS:文章中有些是個人觀點,如果不對,歡迎交流、指正!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM