VueRouter 源码解析(二)VueRouter 安装、初始化、以及创建匹配器

247 阅读4分钟

VueRouter 源码解析(二)VueRouter 安装、初始化、以及创建匹配器

设计思路

什么是路由

维基是这样定义的:路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。路由引导分组转送,经过一些中间的节点后,到它们最后的目的地。作成硬件的话,则称为路由器。路由通常根据路由表——一个存储到各个目的地的最佳路径的表——来引导分组转送

什么是前端路由

前端路由即响应页面内容的任务是由前端来做的,根据不同的url更新页面的内容,随着SPA(单页面应用)的普遍使用,前后端开发分离,项目中基本都使用前端路由,通过路由实现页面的变化。例如,通过vue开发的SPA中,切换路由,并不刷新页面,而是根据路由在虚拟DOM中加载所需要的数据,实现页面内容的改变。

实现思路

  • 首先在 Vue 中使用,我们都需要传入 VueRouter 的配置项
    const router = new Router({
      routes: [{
          path: "/",
          name: "home",
          component: Home
          },
          {
          path: "/about",
          name: "about",
          // route level code-splitting
          // this generates a separate chunk (about.[hash].js) for this route
          // which is lazy-loaded when the route is visited.
          component: () => import( /* webpackChunkName: "about" */ "../views/About.vue"),
          children: [{
              path: 'a',
              component: () => import("../views/about/A.vue")
              },
              {
              path: 'b',
              component: () => import("../views/about/B.vue")
              },
          ]
          }
      ]
    });
    
  • 在实例化 VueRouter 后 我们需要拿到 url 对 url 解析得到 pathList 、 pathMap 等属性 在根据传入的 routes 依次生成 record 对象。
    • record 对象 包含了 path 、 matched 、 component 等属性
  • 在得到 url 和 传入路由的映射关系,我们就方便 通过 '/about' 去渲染 About 组件到界面上了

下面我们就揭开 VueRouter 的真面目。

以下内容都是基于 Hash 模式的视角去举例注释

install

/src/install.js

  • Vue.use 的本质就是执行插件的 install 方法。
  • VueRouter 的安装方法,判断插件有没有安装,避免重复安装【这里 Vue 框架也做了此判断。Vue.use 会去安装的插件添加到 installedPlugins 数组,每次执行 Vue.use 方法会先去 installedPlugins 数组中查找是否有插件,若有就返回当前实例并结束执行】
  • install 方法默认接收到 Vue 也是 Vue.use 的方法完成的。【Vue.use 会定义一个参数列表将 this => Vue 添加到参数数组的第一位,在通过 apply 方法执行插件的 install 方法】
import View from './components/view'
import Link from './components/link'

// 提前声明 Vue,后续 Vue 会做为参数传入,为此将其缓存, 避免 Vue 做为依赖在之后的 build 将 Vue 打包进来
export let _Vue

/**
 * 插件的 install 方法
 * @param {*} Vue Vue 实例
 */
export function install(Vue) {
  // 避免重复安装
  if (install.installed && _Vue === Vue) return
  install.installed = true

  // 将 Vue 实例缓存
  _Vue = Vue
  /** 检测属性是否为未定义 */
  const isDef = v => v !== undefined

  /**
   * 为 router-view 组件关联路由组件
   * @param {*} vm Vue 实例
   * @param {*} callVal Vue 实例
   */
  const registerInstance = (vm, callVal) => {
    // 获取父节点的 vnode
    let i = vm.$options._parentVnode
    // 这里的 i 就是 registerRouteInstance 函数
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  // Vue 的混入方法 VueRouter 实现的核心
  Vue.mixin({
    // this => Vue 实例
    // Vue 生命周期钩子函数
    beforeCreate() {
      if (isDef(this.$options.router)) {
        // 将 Vue 根实例赋值给 _routerRoot
        this._routerRoot = this
        // 这里的 router 就已经是 VueRouter 的实例
        this._router = this.$options.router
        // 调用 VueRouter 的 init 方法
        this._router.init(this)
        // Vue 的响应式处理
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 若实例上没有 router 属性 说明不是 VueRouter 根组件是需要渲染组件
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed() {
      registerInstance(this)
    }
  })

  // 劫持对 $router 的访问,使其返回 _routerRoot._router
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot._router
    }
  })

  // 劫持对 $router 的访问,使其返回 _routerRoot._route
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this._routerRoot._route
    }
  })

  // 将 RouterView 和 ROuterLink 变成 Vue 全局组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  // 对路由钩子使用相同的钩子合并策略
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

new VueRouter

/src/router.js

  • VueRouter 的实体类,router.push 、 router.replace 、 路由的模式(hash 、 HTML5History) 都是在此定义的。
/* @flow */

import { install } from './install'
import { START } from './util/route'
import { assert, warn } from './util/warn'
import { inBrowser } from './util/dom'
import { cleanPath } from './util/path'
import { createMatcher } from './create-matcher'
import { normalizeLocation } from './util/location'
import { supportsPushState } from './util/push-state'
import { handleScroll } from './util/scroll'
import { isNavigationFailure, NavigationFailureType } from './util/errors'

import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'

import type { Matcher } from './create-matcher'

/**
 * VueRouter 实例
 */
export default class VueRouter {
  // install 方法
  static install: () => void
  // 版本信息
  static version: string
  // 导航失败的类型信息
  static isNavigationFailure: Function
  // 导航状态的信息 (重定向、取消 ...)
  static NavigationFailureType: any
  // 开始位置的导航
  static START_LOCATION: Route

  // Vue 实例
  app: any
  apps: Array<any>
  ready: boolean
  readyCbs: Array<Function>
  // 配置项
  options: RouterOptions
  //模式
  mode: string
  // 路由模式对应的实例 history hash abstract
  history: HashHistory | HTML5History | AbstractHistory
  // 匹配器
  matcher: Matcher
  fallback: boolean
  // 全局路由前置守卫钩子
  beforeHooks: Array<?NavigationGuard>
  // 全局路由解析守卫钩子
  resolveHooks: Array<?NavigationGuard>
  // 全局路由守卫钩子
  afterHooks: Array<?AfterNavigationHook>

  /**
   * @param {*} options VueRouter 的配置项
   */
  constructor(options: RouterOptions = {}) {
    if (process.env.NODE_ENV !== 'production') {
      warn(
        this instanceof VueRouter,
        `Router must be called with the new operator.`
      )
    }

    // 当前 Vue 实例 => app.vue
    this.app = null
    // 一个路由可能对应多个组件
    this.apps = []
    // 配置项
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 创造匹配器
    // matcher 中 含有 addroutes match ... 方法
    this.matcher = createMatcher(options.routes || [], this)

    // 路由模式 若配置项没有传递 默认为 hash 模式
    let mode = options.mode || 'hash'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    // 在非浏览器环境
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    // 根据 mode 值来判断 VueRouter 的模式
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

  /**
   * 匹配路径的方法
   * 这里只是方便后续调用,本质还是调用 matcher.match
   * @param {*} raw 原始的位置信息 path => '/about'
   * @param {*} current 当前路由
   * @param {*} redirectedFrom
   * @returns
   */
  match(raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }

  // 当前路由
  get currentRoute(): ?Route {
    return this.history && this.history.current
  }

  // VueRouter 初始化方法
  init(app: any /* Vue component instance */) {
    process.env.NODE_ENV !== 'production' &&
      assert(
        install.installed,
        `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
          `before creating root instance.`
      )

    this.apps.push(app)

    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    app.$once('hook:destroyed', () => {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      // ensure we still have a main app or null if no apps
      // we do not release the router so it can be reused
      if (this.app === app) this.app = this.apps[0] || null

      if (!this.app) this.history.teardown()
    })

    // main app previously initialized
    // return as we don't need to set up new history listener
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    if (history instanceof HTML5History || history instanceof HashHistory) {
      const handleInitialScroll = routeOrError => {
        const from = history.current
        const expectScroll = this.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll

        if (supportsScroll && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from, false)
        }
      }
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }
    // 通过发布订阅模式,更改 _route 的值
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }

  // beforeEach 钩子函数
  beforeEach(fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
  }
  // resolve 钩子函数
  beforeResolve(fn: Function): Function {
    return registerHook(this.resolveHooks, fn)
  }
  // afterEach 钩子函数
  afterEach(fn: Function): Function {
    return registerHook(this.afterHooks, fn)
  }

  onReady(cb: Function, errorCb?: Function) {
    this.history.onReady(cb, errorCb)
  }

  onError(errorCb: Function) {
    this.history.onError(errorCb)
  }

  // router.push 方法
  push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

  // router.replace 方法
  replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
  }

  go(n: number) {
    this.history.go(n)
  }

  back() {
    this.go(-1)
  }

  forward() {
    this.go(1)
  }

  // 获取匹配的组件 没找到引用处 暂时是无用的
  getMatchedComponents(to?: RawLocation | Route): Array<any> {
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if (!route) {
      return []
    }
    return [].concat.apply(
      [],
      route.matched.map(m => {
        return Object.keys(m.components).map(key => {
          return m.components[key]
        })
      })
    )
  }

  /**
   * 解析目标路由位置
   * @param {*} to 目标位置
   * @param {*} current 当前默认的路由
   * @param {*} append 是否允许你在当前默认的路由上附加路径
   */
  resolve(
    to: RawLocation,
    current?: Route,
    append?: boolean
  ): {
    location: Location,
    route: Route,
    href: string,
    // for backwards compat
    normalizedTo: Location,
    resolved: Route
  } {
    current = current || this.history.current
    const location = normalizeLocation(to, current, append, this)
    const route = this.match(location, current)
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)
    return {
      location,
      route,
      href,
      // for backwards compat
      normalizedTo: location,
      resolved: route
    }
  }

  /**
   * 获取路由方法
   * 和 match 方法,都是为了方便使用,本质还是调用 matcher 的 getRoutes 方法
   */
  getRoutes() {
    return this.matcher.getRoutes()
  }

  // 添加 route
  addRoute(parentOrRoute: string | RouteConfig, route?: RouteConfig) {
    this.matcher.addRoute(parentOrRoute, route)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }

  // 动态添加路由
  addRoutes(routes: Array<RouteConfig>) {
    if (process.env.NODE_ENV !== 'production') {
      warn(
        false,
        'router.addRoutes() is deprecated and has been removed in Vue Router 4. Use router.addRoute() instead.'
      )
    }
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}
/**
 * 注册 hook 钩子方法
 * @param {*} list hook 队列数组
 * @param {*} fn hook 方法函数体
 */
function registerHook(list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

/**
 * 创建路径
 * @param {*} base 基础路径 => /hash-mode
 * @param {*} fullPath 完整路径 => /about
 * @param {*} mode 路由模式 => hash
 */
function createHref(base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}

// We cannot remove this as it would be a breaking change
// 插件的 install 方法
VueRouter.install = install

// 版本
VueRouter.version = '__VERSION__'

// 导航失败的类型信息
VueRouter.isNavigationFailure = isNavigationFailure

// 导航状态的信息 (重定向、取消 ...)
VueRouter.NavigationFailureType = NavigationFailureType

VueRouter.START_LOCATION = START

// 是否在浏览器环境
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

createMatcher

/src/create-matcher.js

  • createMatcher 内置的核心方法会抽离出介绍
/**
 * 会创建一个匹配器
 * 转化用户传入的的 router 结构为易于处理的结构
 * { '/':xx , '/ablout':About }
 * @param {*} routes 用户传递的 routes
 * @param {*} router VueRouter 实例
 */
export function createMatcher(
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // 根据路由创建映射关系
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes(routes) {...}

  function addRoute(parentOrRoute, route) {...}

  function getRoutes() {...}

  function match(
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {...}

  function redirect(record: RouteRecord, location: Location): Route {...}


  function alias(
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {...}

  function _createRoute(
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {...}

  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

addRoutes

/src/create-matcher.js

  /**
   * 动态添加路由的方法
   * 将新增的记录 添加到 pathList 、 pathMap 中
   * @param {*} routes
   */
  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

addRoute

/src/create-matcher.js

  /**
   * 动态添加路由的方法 功能同 addRoutes
   * 将新增的记录 添加到 pathList 、 pathMap 中
   * @param {*} parentOrRoute
   * @param {*} route 路由
   */
  function addRoute(parentOrRoute, route) {
    // 若 parentOrRoute 是对象,则从 nameMap 获取对应的 val 否则为 undefind
    const parent =
      typeof parentOrRoute !== 'object' ? nameMap[parentOrRoute] : undefined
    // $flow-disable-line
    createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)

    // add aliases of parent
    // 添加别名
    if (parent && parent.alias.length) {
      createRouteMap(
        // $flow-disable-line route is defined if parent is
        parent.alias.map(alias => ({ path: alias, children: [route] })),
        pathList,
        pathMap,
        nameMap,
        parent
      )
    }
  }

getRoutes

/src/create-matcher.js

  /**
   * 获取所有路由记录列表
   */
  function getRoutes() {
    return pathList.map(path => pathMap[path])
  }

match

/src/create-matcher.js

  /**
   * 用路由匹配对应记录的方法
   * 根据当前的路径找到 pathMap 里面的记录
   * @param {*} raw location
   * @param {*} currentRoute 当前路由
   * @param {*} redirectedFrom 
   */
  function match(
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    // 标准化路径信息返回标准化之后的 localtion (防止访问 http://xxx:8080 不带/ 不识别)
    const location = normalizeLocation(raw, currentRoute, false, router)
    // 解构得到 name
    const { name } = location

    if (name) { // 若 name 存在
      // 从 nameMap 获取对应的记录
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      // 若记录不存在,则新增路由,结束函数执行
      if (!record) return _createRoute(null, location)
      // 记录的正则匹配
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

        // 若获取的参数不是对象,则将其赋值为空对象
      if (typeof location.params !== 'object') {
        location.params = {}
      }
      // 若当前路由存在且参数为对象
      if (currentRoute && typeof currentRoute.params === 'object') {
        // 遍历当前路由的参数对象
        for (const key in currentRoute.params) {
          // 当前路由的参数在路由的正则匹配后的参数能找到
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            // 将当前路由对应参数的值赋值给匹配到路由参数的值
            location.params[key] = currentRoute.params[key]
          }
        }
      }
      // 填充路由信息的参数
      location.path = fillParams(
        record.path,
        location.params,
        `named route "${name}"`
      )
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) { // 没有 name
      location.params = {}
      // 遍历 pathList 创建 route 
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        // 判断路由是否有参数
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // no match
    // 若没有 name 和 path 则创建一个空路由
    return _createRoute(null, location)
  }

_createRoute

/src/create-matcher.js

  /**
   * 创建路由信息的前置方法 用以判断路由是否重定向等
   * @param {*} record 匹配地址的记录 =>{name:'',path:'/',component:Home,...}
   * @param {*} location 位置信息 =>{path:'/',params:{},...}
   * @param {*} redirectedFrom 
   * @returns 
   */
  function _createRoute(
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    // 是否重定向
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    // 创建 route
    return createRoute(record, location, redirectedFrom, router)
  }

createRoute

/src/util/route.js

/**
 * 创建路由
 * @param {*} record 
 * @param {*} location 
 * @param {*} redirectedFrom 
 * @param {*} router 
 */
export function createRoute(
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  // 获取字符串参数
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}
  // { path:'/about/a' ,component : A } 需要先渲染 /about 对应路径的组件 再渲染 /about/a 对应的组件 ,需要创建一个匹配数组
  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    // 匹配路径的路由 /about/a => About A
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  // 返回一个不能修改的 route 对象
  return Object.freeze(route)
}

formatMatch

/src/util/router.js

/**
 * 根据路径匹配的路由
 * '/about/a' => About A
 * @param {*} record 记录
 */
function formatMatch(record: ?RouteRecord): Array<RouteRecord> {
  // 创建一个空的匹配数组
  const res = []
  // 根据记录是否存在循环
  while (record) {
    // 将记录添加进入匹配集合
    res.unshift(record)
    // 将父路由赋值为当前记录
    record = record.parent
  }
  return res
}

createRouteMap

/src/create-router-map.js

/**
 * 扁平化(格式化)用户传进来的 routes
 * 创建路由映射 map
 * ['/','/about','/about/a']  => {'/':记录,'/about':记录,'/about/a':记录}
 * @param routes 用户传入的 routes
 * @param oldPathList 老的路径数组
 * @param oldPathMap 老的路由对应关系 Pathmap
 * @param oldNameMap 老的路由对应关系 Namemap
 * @param parentRoute 路由记录 record
 */
export function createRouteMap(
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>,
  parentRoute?: RouteRecord
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  // path 匹配数组 => ['/','/about','/about/a']
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  // path 匹配的对象 => {'/':记录,'/about':记录,'/about/a':记录}
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  // name 匹配的对象
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  // 遍历 用户传入的 routes 添加记录到 pathList 、 pathMap 、 nameMap
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })

  // 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--
    }
  }

  if (process.env.NODE_ENV === 'development') {
    // warn if routes do not include leading slashes
    const found = pathList
      // check for missing leading slash
      .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')

    if (found.length > 0) {
      const pathNames = found.map(path => `- ${path}`).join('\n')
      warn(
        false,
        `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`
      )
    }
  }

  return {
    pathList,
    pathMap,
    nameMap
  }
}

addRouteRecord

/src/create-router-map.js

/**
 * 添加路由记录
 * @param {*} pathList 路径数组
 * @param {*} pathMap 路径映射关系
 * @param {*} nameMap 名称映射关系
 * @param {*} route 路由信息
 * @param {*} parent 父路由
 * @param {*} matchAs
 */
function addRouteRecord(
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  // 解构得到 path 和 name
  const { path, name } = route
  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.`
    )

    warn(
      // eslint-disable-next-line no-control-regex
      !/[^\u0000-\u007F]+/.test(path),
      `Route with path "${path}" contains unencoded characters, make sure ` +
        `your path is correctly encoded before passing it to the router. Use ` +
        `encodeURI to encode static segments of your path.`
    )
  }

  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
  // 标准化 path 信息
  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 },
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    instances: {},
    enteredCbs: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
        ? route.props
        : { default: route.props }
  }

  // 如果该路由还有子路由
  if (route.children) {
    // 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.`
        )
      }
    }
    // 遍历添加子路由
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }

  // 若果路由别名存在
  if (route.alias !== undefined) {
    // 判断别名是否为一个数组
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
    // 遍历别名数组
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if (process.env.NODE_ENV !== 'production' && alias === path) {
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }

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

  // 如果 name 存在
  if (name) {
    if (!nameMap[name]) {// 在 nameMap 找不到该 name 路由的对应关系
      // 赋值
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

PS

  • 代码仓库地址觉得对你有帮助记得 🌟 Star 哦
  • 后续会持续更新相关内容以及其他内容
  • 觉得文章不错的话,记得 👍 🌟 嘻嘻
  • 如有不足欢迎指正