SPA 路由三部曲之核心原理

jdf2e發表於2020-11-17

為了配合單頁面 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:文章中有些是個人觀點,如果不對,歡迎交流、指正!

相關文章