[Vue.js進階]從原始碼角度剖析vue-router(上)

yeyan1996發表於2019-05-14

image

前言

Vue 是一個漸進式的框架,這意味著你可以只使用 Vue 的核心庫來開發,但是當你在開發一個完整的業務專案時,路由是一個必不可少的部分

在曾經的前端領域中,一直都使用的是服務端渲染的模式,即使用者輸入 url 後,瀏覽器向伺服器請求這個 url 對應的HTML,伺服器返回 HTML給前端,前端再展示,然後當需要瀏覽別的頁面時,需要點選 a 標籤再向伺服器傳送一個請求,伺服器就會再發給你目標頁面的 HTML

這樣會暴露一些缺點:

  • 每次跳轉都向伺服器請求,會增加伺服器的壓力

  • 每次跳轉都會重新整理頁面導致跳轉過程中會有一瞬間的白屏,使用者體驗不是非常好

  • 由於是服務端渲染,受到 XSS 的攻擊可能性也較高

在 MVVM 框架興起的同時,越來越多的開發者傾向於使用前端渲染的模式,服務端返回固定 JS 檔案給前端,瀏覽器執行 JS 檔案再渲染出整個頁面,而在路由方面,前端會維護一個路由的層級樹,當輸入 url 後,不再向後端請求 HTML,而是去這個層級樹中找到對應頁面的 JS 檔案並執行,從而渲染出新的頁面,整個過程是純前端控制的,所以也被稱為前端路由

而 vue-router 作為 Vue 的路由庫,它是怎麼實現路由地址和元件之間的轉換的呢,這篇文章中,我將會帶大家深入 vue- router 的原始碼,解密 vue-router API 背後的原理

文中的原始碼截圖只保留核心邏輯 完整原始碼地址

需要了解一些 Vue 的公共函式(mixins,install,defineReactive)

vue-router 版本:3.0.2

vue-router的使用方法

我們從 vue-router 的使用方法說起,當使用 vue-router 時,一般會分為3步

  1. 引入 vue-router,呼叫 Vue.use(Router)

  2. 例項化 router 物件,傳入一個路由層級表 routes

  3. 在 main.js 中給根例項傳入 router 物件

註冊 vue-router 外掛

當我們呼叫 Vue.use(Router)時會執行外掛的註冊流程

圖1:

image

所有的 Vue 外掛都會暴露一個 install 方法,當執行 Vue.use 時,實質上 Vue 會執行外掛的 install 方法

混入全域性鉤子

瞭解過 Vue 響應式原理的朋友可以發現,vue-router 會通過 Vue.mixin 的方法全域性混入 beforeCreate,destroyed 2個鉤子,因為是全域性混入的,所以之後所有的根例項和元件例項都會有這2個生命週期鉤子

當根例項被例項化時,混入的 beforeCreate 第一次被執行,因為我們在 new Vue 時傳入了 router 物件,它會被 Vue 作為 $options 的屬性,所以會執行到 true 的邏輯,這裡的核心在於 init 方法,它會初始化整個 vue-router 我們之後詳解,另外將傳入的 router 物件變成一個響應式物件,這個我們也之後討論

除開根例項,其餘所有的元件例項都會執行 false 的邏輯,它會給元件例項定義一個 _routerRoot 屬性,因為 Vue 生成元件時是從上到下的,所以所有元件例項的 _routerRoot 屬性都指向根例項

之後執行 registerInstance 這個也放到後面討論

定義 $router,$route 屬性

隨後 Vue 在原型上定義了 $router,$route 2個物件,攔截 get 方法指向 _routerRoot.router,從上面一章可以發現,實質上指向的就是根例項的 router 物件,即日常開發中呼叫的 this.$router 最終都會指向根例項上的 router 物件

定義全域性元件

最後通過 Vue.component 方法註冊了2個全域性元件,這樣我們可以在任何地方直接使用<router-view>和<router-link>元件

例項化 vue-router

通常使用 vue-router 時,會在 router.js 中通過 new Router 的形式生成一個 router 的例項,並傳入一個路由的層級表 routes 陣列

圖2:

[Vue.js進階]從原始碼角度剖析vue-router(上)

隨後我們找到原始碼中的 vue-router 類

圖3:

image

整個 vue-router 例項化的過程核心就做了2件事

  1. 通過 createMatcher 建立了一個物件賦值給例項的 matcher 屬性

  2. 根據傳入的 mode 屬性例項化不同的 history 路由例項

建立路由的對映表

圖中第四行會執行到 createMatcher 方法,返回一個物件,包含 matchaddRoutes 這2個方法,這2個方法是 vue-router 中比較重要的函式,之後我們會分析它們的作用,在這之前先看一下 createMatcher 函式中的 createRouteMap 函式

圖4:

image

createRouteMap 這個函式就是用來建立路由的對映表的,它是一個記錄所有資訊(路由記錄)的物件,將傳入的 routes 陣列進行一系列處理,生成 pathList,pathMap,nameMap 3張路由對映表

圖5:

image

createRouteMap 內部會遍歷 routes 陣列,執行 addRouteRecord 方法來為**每一個陣列的每個元素(route 物件)建立記錄,並儲存在這3個路由對映表中

圖6:

image

addRouteRecord 會將每個 route 物件轉換為一個路由記錄並儲存在之前宣告的3個路由對映表中,通過原始碼發現,路由記錄(record 物件)非常詳細的記錄了 route 物件的很多屬性

  • path:路由的完整路徑

  • regex:匹配到當前 route 物件的正則

  • components:route 物件的元件(因為 vue-router 中有命名檢視,所以會預設放在 default 屬性下,instances 同理)

  • instances: route 物件對應的 vm 例項

  • name:route 物件的名字

  • parent:route 物件的父級路由記錄

  • matchAs:路由別名

  • redirect:路由重定向

  • beforeEnter:元件級別的路由鉤子

  • meta:路由元資訊

  • props:路由跳轉時的傳參

在建立路由記錄前,會使用 normalizedPath 規範化 route 物件的路徑,如果傳入的 route 物件含有父級 route 物件,會將父級 route 物件的 path 拼上當前的 path

圖7:

image

例如圖2中的 comp1Child 這個 route 物件,它的 path 最終會變成

"/comp1" + "comp1Child" => "/comp1/com1Child"

而最終會生成的路由記錄是這樣的

圖8:

[Vue.js進階]從原始碼角度剖析vue-router(上)

隨後因為 route 可能含有 children 屬性,即含有子的 route 物件組成的陣列,所以需要進行遞迴的遍歷,然後將 record 物件放入這3個路由對映表中,而這3個路由對映表的區別在於

  • pathList:陣列,儲存了 route 物件的路徑

  • pathMap:物件,儲存了所有 route 物件對應的 record 物件

  • nameMap:物件,儲存了所有含有name屬性的 route 物件對應的 record 物件

圖2中的路由對應的3張路由對映表如下:

pathList:

[Vue.js進階]從原始碼角度剖析vue-router(上)

pathMap:

[Vue.js進階]從原始碼角度剖析vue-router(上)

nameMap:

[Vue.js進階]從原始碼角度剖析vue-router(上)

可以看到 pathMap 和 nameMap 是一樣的,因為圖2中的路由都有 name 屬性,如果某個路由沒有 name 屬性,則只會在 pathMap 中存在

對比儲存了所有 route 物件的 routes 陣列和這3個路由對映表,我們可以發現:routes 物件是一個遞迴的樹形結構,而路由對映表是一個扁平的一維結構,通過路由對映表裡的 parent 屬性來維護父子關係

動態新增路由的 addRoutes 函式

在建立完路由對映表後,會向外暴露一個動態新增路由的 API addRoutes

圖10:

image

它的原理其實很簡單,就是接受一個 route 物件,並且把它轉換成 record 物件,然後合併到之前生成的路由對映表中,所以我們可以在外部呼叫 router.addRoutes 動態註冊路由

返回 $route 物件的 match 函式

createMatcher 返回的第二個函式是 matchmatch 函式會返回一個 route 物件

圖11:

image

之前說的 route 是針對 new Router 時傳入的 routes 陣列的每個元素,而 $route 是最終返回作為 Vue.prototype.$route 使用的物件,在 flow 語言中,route 的型別是 RouteConfig,而 $route 的型別是 Route,具體介面的定義可以檢視原始碼,雖然在原始碼中兩者變數名都是 route,但我下文會使用 $route 來區分通過 this.$route 返回 route 物件

圖12:

routes :

[Vue.js進階]從原始碼角度剖析vue-router(上)

$route :

[Vue.js進階]從原始碼角度剖析vue-router(上)

前者表示的是路由的一些基礎配置項,而後者是真正經過 vue-router 處理後表示當前路由的物件

每次路由跳轉的時候都會執行這個 match 函式生成一個 $route 物件,具體什麼時候會觸發 match 放到下篇中講,這章先分析 match 函式是如何最終生成一個真正的 $route 物件的

生成 loaction 物件

match函式首先會執行 normalizeLocation 函式,它是一個輔助函式,會將呼叫 router.push / router.replace 時跳轉的路由地址轉為一個 location 物件

那什麼是 location 物件? MDN 上是這麼解釋的

Location介面表示其連結到的物件的位置(URL)。所做的修改反映在與之相關的物件上。 DocumentWindow 介面都有這樣一個連結的Location,分別通過 Document.locationWindow.location 訪問。

通俗的來說就是用一個物件來描述當前 url 的一些資訊。當我們在位址列中輸入 www.baidu.com ,按 F12 開啟控制檯,輸入 loaction 就能展示出當前地址的一些資訊

圖13:

image

vue-router 在 location 介面的基礎上做了一些增強,新增了 name,path,hash 等 vue-router 特有的屬性

舉個例子,當呼叫 router.push({name:"comp1"}) 使用 name 的形式進行路由跳轉時,返回的 loaction 物件就會有一個 name 屬性,當 name 存在時,會走到圖11中的 true 邏輯,從之前 createMatcher 生成的 nameMap 路由對映表中找到對應 name 的路由記錄 record 物件,最終會執行 _createRoute 這個方法

而呼叫 router.push("/comp1") 使用路徑的形式進行路由跳轉,同樣也會返回一個 location 物件,但不會有 name 屬性,走圖11的 false 邏輯,從另外2個路由對映表 pathMap,pathList 中找到對應的路由記錄,最終也會執行 _createRoute 這個方法

可見無論使用 name 跳轉還是使用 path 跳轉,最終都會執行 _createRoute ,帶下劃線的 _createRoute 是一個私有方法,它最終會呼叫 createRoute 生成 $route 物件

生成 $route 物件

圖14:

image

經過對一些 query 引數的處理,最終返回 $route 物件,其中有一個 matched 屬性值得注意,它通過 formatMatch 函式生成,檢視過 this.$route 返回值的朋友應該知道,matched 是一個陣列,每個元素都是一個路由記錄(record)

圖15:

image

還記得之前在生成路由記錄的時定義的 parent 屬性嗎?它的其中一個用途就是通過不斷的向上查詢父級的路由記錄,放入 matched 陣列中,最終返回一個儲存了當前路由記錄和所有父級陣列,順序是 父 => 子

圖16:

[Vue.js進階]從原始碼角度剖析vue-router(上)

而這個 matched 陣列最終會決定觸發哪些路由元件的哪些路由守衛鉤子,關於路由鉤子部分我們放到下篇來說

生成 history 路由例項

再次回到圖3,vue-router 根據傳入引數的 mode 屬性來例項化不同的路由類(HTML5,hash,abstract),這也是官方提供給開發者的3種不同的選擇來生成路由

  • HTML5 路由是相對比較美觀的一種路由,和正常的 url 顯示沒有什麼區別,核心依靠 pushStatereplaceState 來實現不向後端傳送請求的路由跳轉,但是當使用者點選重新整理按鈕時會存在找不到頁面的情況,需要配合 nginx 來做一層轉發

  • hash 路由是預設使用的路由,在 url 中會存在一個 # 號,核心依靠這個 # 號也就是曾經作為路由的錨點來實現不向後端傳送請求的路由跳轉

  • abstract 路由是一種抽象路由,一般用在非瀏覽器端,維護一種抽象的路由結構,使得能夠嫁接在客戶端或者服務端等沒有 history 路由的地方

流程圖

這裡畫了一張流程圖來表達例項化 vue-router 時的 matcher 屬性內部的依賴關係

[Vue.js進階]從原始碼角度剖析vue-router(上)

總結

  • 當呼叫 Vue.use(Router) 時,會給全域性的 beforeCreate,destroyed 混入2個鉤子,使得在元件初始化時能夠通過 this.$router / this.$route 訪問到根例項的 router / route 物件,同時還定義了全域性元件 router-view / router-link

  • 在例項化 vue-router 時,通過 createRouteMap 建立3個路由對映表,儲存了所有路由的記錄,另外建立了 match 函式用來建立 $route 物件,addRoutes 函式用來動態生成路由,這2個函式都是需要依賴路由對映表生成的

  • vue-router 還給開發者提供了3種不同的路由模式,每個模式下的跳轉邏輯都有所差異

vue-router 定義了 match 方法用來生成 $route 物件,而什麼時候會呼叫 match 方法還沒有分析過,另外文章開頭的 registerInstance 又是做什麼的,在下篇中我會分析 vue-router 中的跳轉邏輯,包括路由守衛,vue-router 的全域性元件,以及元件相關的檢視更新

參考資料

Vue.js 技術揭祕

相關文章