前言
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步
-
引入 vue-router,呼叫 Vue.use(Router)
-
例項化 router 物件,傳入一個路由層級表 routes
-
在 main.js 中給根例項傳入 router 物件
註冊 vue-router 外掛
當我們呼叫 Vue.use(Router)時會執行外掛的註冊流程
圖1:
所有的 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-router 類
圖3:
整個 vue-router 例項化的過程核心就做了2件事
-
通過
createMatcher
建立了一個物件賦值給例項的 matcher 屬性 -
根據傳入的 mode 屬性例項化不同的 history 路由例項
建立路由的對映表
圖中第四行會執行到 createMatcher
方法,返回一個物件,包含 match
和 addRoutes
這2個方法,這2個方法是 vue-router 中比較重要的函式,之後我們會分析它們的作用,在這之前先看一下 createMatcher
函式中的 createRouteMap
函式
圖4:
而 createRouteMap
這個函式就是用來建立路由的對映表的,它是一個記錄所有資訊(路由記錄)的物件,將傳入的 routes 陣列進行一系列處理,生成 pathList,pathMap,nameMap 3張路由對映表
圖5:
createRouteMap
內部會遍歷 routes 陣列,執行 addRouteRecord
方法來為**每一個陣列的每個元素(route 物件)建立記錄,並儲存在這3個路由對映表中
圖6:
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:
例如圖2中的 comp1Child 這個 route 物件,它的 path 最終會變成
"/comp1" + "comp1Child" => "/comp1/com1Child"
而最終會生成的路由記錄是這樣的
圖8:
隨後因為 route 可能含有 children 屬性,即含有子的 route 物件組成的陣列,所以需要進行遞迴的遍歷,然後將 record 物件放入這3個路由對映表中,而這3個路由對映表的區別在於
-
pathList:陣列,儲存了 route 物件的路徑
-
pathMap:物件,儲存了所有 route 物件對應的 record 物件
-
nameMap:物件,儲存了所有含有name屬性的 route 物件對應的 record 物件
圖2中的路由對應的3張路由對映表如下:
pathList:
pathMap:
nameMap:
可以看到 pathMap 和 nameMap 是一樣的,因為圖2中的路由都有 name 屬性,如果某個路由沒有 name 屬性,則只會在 pathMap 中存在
對比儲存了所有 route 物件的 routes 陣列和這3個路由對映表,我們可以發現:routes 物件是一個遞迴的樹形結構,而路由對映表是一個扁平的一維結構,通過路由對映表裡的 parent 屬性來維護父子關係
動態新增路由的 addRoutes 函式
在建立完路由對映表後,會向外暴露一個動態新增路由的 API addRoutes
圖10:
它的原理其實很簡單,就是接受一個 route 物件,並且把它轉換成 record 物件,然後合併到之前生成的路由對映表中,所以我們可以在外部呼叫 router.addRoutes 動態註冊路由
返回 $route 物件的 match 函式
createMatcher
返回的第二個函式是 match
,match
函式會返回一個 route 物件
圖11:
之前說的 route 是針對 new Router 時傳入的 routes 陣列的每個元素,而 $route 是最終返回作為 Vue.prototype.$route 使用的物件,在 flow 語言中,route 的型別是 RouteConfig,而 $route 的型別是 Route,具體介面的定義可以檢視原始碼,雖然在原始碼中兩者變數名都是 route,但我下文會使用 $route 來區分通過 this.$route 返回 route 物件
圖12:
routes :
$route :
前者表示的是路由的一些基礎配置項,而後者是真正經過 vue-router 處理後表示當前路由的物件
每次路由跳轉的時候都會執行這個 match
函式生成一個 $route 物件,具體什麼時候會觸發 match
放到下篇中講,這章先分析 match
函式是如何最終生成一個真正的 $route 物件的
生成 loaction 物件
match
函式首先會執行 normalizeLocation
函式,它是一個輔助函式,會將呼叫 router.push / router.replace 時跳轉的路由地址轉為一個 location 物件
那什麼是 location 物件? MDN 上是這麼解釋的
Location
介面表示其連結到的物件的位置(URL)。所做的修改反映在與之相關的物件上。Document
和Window
介面都有這樣一個連結的Location,分別通過Document.location
和Window.location
訪問。
通俗的來說就是用一個物件來描述當前 url 的一些資訊。當我們在位址列中輸入 www.baidu.com
,按 F12 開啟控制檯,輸入 loaction 就能展示出當前地址的一些資訊
圖13:
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:
經過對一些 query 引數的處理,最終返回 $route 物件,其中有一個 matched 屬性值得注意,它通過 formatMatch
函式生成,檢視過 this.$route 返回值的朋友應該知道,matched 是一個陣列,每個元素都是一個路由記錄(record)
圖15:
還記得之前在生成路由記錄的時定義的 parent 屬性嗎?它的其中一個用途就是通過不斷的向上查詢父級的路由記錄,放入 matched 陣列中,最終返回一個儲存了當前路由記錄和所有父級陣列,順序是 父 => 子
圖16:
而這個 matched 陣列最終會決定觸發哪些路由元件的哪些路由守衛鉤子,關於路由鉤子部分我們放到下篇來說
生成 history 路由例項
再次回到圖3,vue-router 根據傳入引數的 mode 屬性來例項化不同的路由類(HTML5,hash,abstract),這也是官方提供給開發者的3種不同的選擇來生成路由
-
HTML5 路由是相對比較美觀的一種路由,和正常的 url 顯示沒有什麼區別,核心依靠
pushState
和replaceState
來實現不向後端傳送請求的路由跳轉,但是當使用者點選重新整理按鈕時會存在找不到頁面的情況,需要配合 nginx 來做一層轉發 -
hash 路由是預設使用的路由,在 url 中會存在一個 # 號,核心依靠這個 # 號也就是曾經作為路由的錨點來實現不向後端傳送請求的路由跳轉
-
abstract 路由是一種抽象路由,一般用在非瀏覽器端,維護一種抽象的路由結構,使得能夠嫁接在客戶端或者服務端等沒有 history 路由的地方
流程圖
這裡畫了一張流程圖來表達例項化 vue-router 時的 matcher 屬性內部的依賴關係
總結
-
當呼叫 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 的全域性元件,以及元件相關的檢視更新