vue-router 源碼探究——路由重置實現

原文發佈於個人 博客, 未經受權禁止轉載html

在以前的一篇博文中主要闡述了前端權限控制的一種實現 —— 前端權限控制的基本實現。其中介紹了經過權限過濾實現動態地私有路由添加,那麼在當前用戶登出時,應該是要重置當前應用的用戶數據的。那麼全局的 vuex 狀態可經過官方替換 store 的方法 replaceState 來實現。那麼在沒有官方實現的 feature 的狀況下該如何刪除(重置)經過 addRoutes 方法添加的動態路由?前端

TL;DR

起初,只有經過調用全局的原生 location.refresh 方法來實現整個頁面的刷新,進而實現當前路由的重置。那麼會有一種在不刷新當前頁面的方法實現當前路由的重置功能麼?在通過一系列的嘗試,在官方源碼存在這樣一個 issue —— feature request: replace routes dynamically,其中提到了一種 hack 的方法,經過替換路由實例的 matcher 對象來實現路由的 重置,即實現刪除經過 addRoutes 添加的路由。vue

那麼截至如今就有兩種解決方案可實現路由的刪除:git

  • 經過調用全局的 location.refresh 方法來實現應用刷新來實現前端路由的重置。github

  • 經過替換當前 vue-routermatcher 屬性對象來實如今不刷新頁面的狀況下重置當前路由實例。vue-router

下文將着重從與 matcher 相關的 vue-router 源碼解讀爲何替換 vue-routermatcher 對象可實現刪除 addRoutes 添加的路由。另外截至本文寫做日期,vue-router 的最新版本爲 v3.0.6。後文所述的全部源碼解讀都是基於 此 v3.0.6 版本vuex

首先在 src/index.js 中可見 vue-router 類,其中包含了一系列咱們熟知的 router API。這裏尤爲要注意與本文相關聯的 VueRouter構造函數 constructor原型方法 addRoutesapi

後文的內容都是基於這個兩個點展開直至解決咱們的核心問題 —— 爲何替換實例的 matcher 可實現 刪除addRoutes 添加的路由。數據結構

先問是什麼,再問爲何。ide

matcher 是什麼

在源碼的 src/index.js 入口文件中的 VueRouter 類的構造函數中可見,路由實例的 matcher 對象由 create-matcher 中的內部方法 createMatcher 建立 而來。

// ... other code

import { createMatcher } from './create-matcher'

// ... other code

export default class VueRouter {
  // ...

  constructor(options: RouterOptions = {}) {
    // ...
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'

    // ... other code
  }

  // ... other code

  addRoutes(routes: Array<RouteConfig>) {
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}
複製代碼

經過對 VueRouter 類的代碼抽象顯而易見:

  • 在實例化路由對象時,會建立一個與當前路由實例對應的 matcher。並在實例化時,傳入在實例化時的 routes 參數。這裏 留心 這裏傳入的 routes 參數,後續的源碼分析也會用到該 routes 參數。

  • 在咱們以前在 前端權限控制的基本實現 中用到的 addRoutes 方法中,本質上是調用了 matcheraddRoutes 方法。

不管是 VueRouter 實例化仍是經過 addRoutes 方法,都繞不開 matcher。那麼再次深刻 create-matcher.js 源碼文件中,可見如下代碼:

// {projectRoot}/src/create-matcher.js

export function createMatcher( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // ...
  return {
    match,
    addRoutes
  }
}
複製代碼

這裏暫時性省略了其餘無關代碼,關注咱們以前的問題 —— matcher 是什麼。在這裏能夠很明顯地看出,實例的 matcher 對象是由 match 屬性和 addRoutes 屬性組成。接下來咱們進一步探究兩者的本質。

這裏先提一點,探究源碼的時候最好是帶着一個具體的問題來看源碼,來理解其中的代碼邏輯,這樣纔不至於在源碼中迷失,以保持本身的初心。

全局路由的存儲

在上一章節,已經提到不管是 路由實例化 仍是調用動態添加路由的 API —— addRoutes。都會調用到一個 createMatcher 函數。這裏咱們將分步討論如下 createMatcher 函數 到底具備什麼樣的職責。

export default class VueRouter {
  constructor(options: RouterOptions = {}) {
    // ...
    this.matcher = createMatcher(options.routes || [], this)
    // ...
  }

  // ...

  match(raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }

  // ...

  addRoutes(routes: Array<RouteConfig>) {
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }

  // ...
}
複製代碼

在查看 create-matcher.js 時,咱們可見第一句代碼:

const { pathList, pathMap, nameMap } = createRouteMap(routes)
複製代碼

這句代碼咱們大體從調用的函數名稱上 推測 一下,在執行 createMatcher 函數 時,首要任務時對當前開發者傳入的 options.routes 進行路由 映射化處理,並獲得了三個路由容器 pathListpathMapnameMap

create-matcher.js 的同級目錄下咱們能夠找到 createRouteMap 所在的文件 create-route-map.js,咱們一樣可將 createRouteMap 的邏輯展現以下:

export function createRouteMap( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  /** * @description Dictionary 泛型 * https://github.com/vuejs/vue-router/blob/v3.0.6/flow/declarations.js#L20 */
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  /** * @description 將全部的 VueRouter 構造函數提供的 routes 分別存入 pathList, * pathMap, nameMap 三個路由容器中。 */
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  /** * @description 由於上文代碼註釋已經說明 pathList 是用於保證路由匹配的優先級 * 那麼,如下代碼用於保證路由通配符始終被最後匹配 */
  // ensure wildcard routes are always at the end
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }

  return {
    pathList,
    pathMap,
    nameMap
  }
}
複製代碼

一樣可知,createRouteMap 函數返回三個屬性 —— pathListpathMapnameMap

  • pathList,做者已經給出了註釋說明,pathList 是用來保證進行非命名路由時的 path 匹配優先級的(具體可查看文檔 —— 匹配優先級)。

在進一步探究 addRouteRecord 時,咱們能夠發現 addRouteRecord 就主要作了三件事,將全部以前實例化時傳入的 options.routes 格式化

  1. 對其中每個 route 作映射,該映射集合被稱爲 pathMap。同時在映射時,不斷向 pathList 列表加入當前 path 記錄以保證匹配路由時的優先級。

  2. 對提供了 name 字段的路由記錄,加入到 nameMap 映射中。由於 name 具備惟一性,因此此時在 nameMap 中就不用考慮匹配優先級了。

  3. 遞歸全部路由的子路由,並進行映射化處理。

具體代碼解析以下:

function addRouteRecord( pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) {
  const { path, name } = route
  // 斷言傳入的 route 中是否包含 path 屬性
  if (process.env.NODE_ENV !== 'production') {
    assert(path != null, `"path" is required in a route configuration.`)
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String( path || name )} cannot be a ` + `string id. Use an actual component instead.`
    )
  }

  // 有沒有提供客製化解析參數
  // https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#高級匹配模式
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}

  // 格式化路由,如拼接子路由等
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  // 匹配時,是否對大小寫敏感
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }

  // 路由記錄對象
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
        ? route.props
        : { default: route.props }
  }

  // 當前 route 存在子路由時
  if (route.children) {
    /** * @description 用來解決子路由爲空字符串或爲 '/' 時,將致使子路由沒法渲染的 BUG * 具體可見:https://github.com/vuejs/vue-router/issues/629 */
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    if (process.env.NODE_ENV !== 'production') {
      if (
        route.name &&
        !route.redirect &&
        route.children.some(child => /^\/?$/.test(child.path))
      ) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
            `When navigating to this named route (:to="{name: '${ route.name }'"), ` +
            `the default child route will not be rendered. Remove the name from ` +
            `this route and use the name of the default child route for named ` +
            `links instead.`
        )
      }
    }
    /** * @description 遞歸映射化當前路由的全部子路由 */
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  /** * @description route.alias 表示路由別名。當存在 /a 別名爲 /b 時,訪問 /b 就像 * 訪問 /a 同樣,但路由仍是保持爲 /b。 * https://router.vuejs.org/guide/essentials/redirect-and-alias.html#alias * https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html#別名 */
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]

    aliases.forEach(alias => {
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    })
  }

  /** * @description 若當前 pathMap 映射容器中不包含 path 路由記錄,那麼將該路由記錄對 * 象添加到 pathMap 容器中。另外 pathList 用來保證匹配的優先級 */
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  /** * @description 若當前路由提供了 name 字段,即當前路由是命名路由時,將在 nameMap * 映射容器中添加該命名路由的路由記錄。 */
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}
複製代碼

如今咱們能夠大體 總結 一下,全部以前咱們經過 VueRouter 構造函數所傳遞的參數 最終 都在 addRouteRecord 函數中獲得解析處理。

  • 全部的路由信息 path 字段都存儲在 pathMap 中,經過 pathList 列表實現 匹配優先級

  • 若存在路由有 name 字段時,該命名路由將被存儲在 nameMap 映射容器中,由於文檔中約定了 name 字段具備惟一性,那麼命名路由沒有專門的 nameList 來實現匹配優先級。

  • 全部路由的每一項子路由都會遞歸進行解析並存儲。

如今咱們理解了全部路由信息的最終歸宿以後,回溯以前的解析能夠發現:

// {ProjectRoot}/src/index.js
import { createMatcher } from './create-matcher'
// ...
export default VueRouter {
  constructor (options: RouterOptions = {}) {
    this.matcher = createMatcher(options.routes || [], this)
  }
}
複製代碼
// {ProjectRoot}/src/create-matcher.js
import { createRouteMap } from './create-route-map'
// ...

export function createMatcher( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // ...
}
複製代碼
// {ProjectRoot}/src/create-route-map.js
export function createRouteMap( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord> ): {
  pathList: Array<string>
  pathMap: Dictionary<RouteRecord>
  nameMap: Dictionary<RouteRecord>
} {
  // ...
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  // ...
}
複製代碼

以上代碼展現了傳入的 options.routes 的格式化存儲過程,最終全部的路由信息都在 addRouteRecord 被解析,並存儲在 pathListpathMap 中,如果命名路由,另外還會被存儲在 nameMap 中。**在構造函數中可見,這一切的路由信息容器都是被掛載在路由實例的 matcher 對象上的。**這一點,對於後續的動態路由刪除功能提供了契機。

matcher.match 函數

以前咱們知道了 matcher 對象由 match 方法和 addRoutes 組成的。咱們大體看一下 match 屬性是指什麼,在 create-matcher.js 中的 26 - 72 行就是咱們找的 match 屬性——它是一個函數。

function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route {
  // 經過調用的函數名稱可大體推斷是格式化當前的路由對象
  const location = normalizeLocation(raw, currentRoute, false, router)

  // 抽離當前路由對象中的 name 字段,即命名路由中的名稱
  const { name } = location

  // 若當前路由是命名路由時
  if (name) {
    // ... code
    // 當前路由不是命名路由,那麼直接進行路由的 path 字段路由匹配
  } else if (location.path) {
    // ... code
  }
  // no match
  return _createRoute(null, location)
}
複製代碼

這裏直接將 match 函數的大體脈絡抽象爲以上結構,通過抽象後的代碼可輕易看出 match 函數的主要定位是 vue-router路由匹配模塊。一切路由的匹配都是依賴於實例的 matcher.match 函數。這裏咱們主要是要探究替換 matcher 爲何能夠實現路由重置,將不對 路由匹配模塊 作深刻探究,若是讀者感興趣的話,能夠從這個函數開始開起,本身能夠嘗試着探究 vue-router 的路由匹配模塊。稍微提示一下,整個 vue-router 實例都是基於命名路由的 name 字段的 nameMap 映射 用於命名路由的路由匹配,使用非命名路由的 pathList 列表 保證路由匹配的優先級和使用 pathMap 映射 來實現路由的匹配的。這裏的匹配搜索原理和做者以前的 前端權限控制的基本實現 數據搜索原理都是基於 映射 這種數據結構。

本文所述的映射是指的一種抽象化的 key-value 數據結構。每個惟一個 key 都有一個惟一的 value 值與之對應。在 JS 中,一個樸素對象或一個 Map 實例對象均可稱爲映射。

動態添加路由的實現

在前文中已經提到咱們外部調用路由實例的 addRoutes 方法 本質 上就是調用了 match.addRoutes 方法實現 路由的動態添加

回到以前的 create-matcher.js 中的 create-matcher 函數,在 22 - 24 行可看到 addRoutes 一樣是一個函數,而且咱們還知道了 addRoutes 的本質就是調用了前文所述的 createRouteMap 函數,咱們對以前全局靜態路由的解析存儲流程有了理解以後就不難理解,調用 createRouteMap 函數本質上就是 向當前路由實例的路由容器動態地添加路由

// {ProjectRoot}/src/index.js
import { createMatcher } from './create-matcher'
// ...
export default VueRouter {
  // ...
  addRoutes (routes: Array<RouteConfig>) {
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}
複製代碼
// {ProjectRoot}/src/create-matcher.js
import { createRouteMap } from './create-route-map'

export function createMatcher( routes: Array<RouteConfig>, router: VueRouter ): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // ...
  return {
    match,
    addRoutes
  }
}
複製代碼

由上源碼可知,調用路由實例的 addRoutes 方法本質上是調用了路由實例的 addRoutes 方法,該方法在其內部完成了傳入的 addRoutes 的路由解析並進行映射化處理。

結論

在咱們明白了 addRoutes 是如何向當前路由實例 動態地添加路由 後,咱們再結合以前的路由實例化中的路由映射化處理流程可知:

替換當前路由實例的 matcher 之因此能實現刪除動態添加的路由,是由於替換當前路由的 matcher 本質 上是 替換了現有的路由實例的路由映射容器。新的 matcher 始終 僅僅 包含路由實例化時的路由,而 不會包含 後期被 addRoutes 方法添加的路由,那麼替換當前路由的 matcher 就可實現刪除經過 addRoutes 添加的路由。

相關文章
相關標籤/搜索