VueRouter 是如何工作的?

317 阅读7分钟

VueRouterVue.js的路由管理器,包含的功能如下:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为 基本用法:
 // 引入Vue 和 VueRouter
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)

const component = {
  template: '<div></div>'
}
// 初始化路由
const router = new VueRouter({
    mode: 'hash', // hash history abstract(node)
    routes: [
        {
          path: '/', // 常规路由
          component
        },
        {
          path: '/news/:id', // 动态路由
          component 
        },
        {
          path: '/category', // 嵌套路由
          component: {
            template: '<router-view></router-view>'
          },
          children: [ // 子路由
            {
              path: 'news',
              component
            }
          ]
        },
        {
          path: '/redirect', // 路由重定向
          redirect: '/home'
        },
        {
          path: '*', // 通配符
          component
        },
        
    ]
});

// 将 router 添加到 Vue的 $options 上
new Vue({
  router
}).$mount('#app')

我们就上面的基本结构进行分析,VueRouter 是如何工作的;

Vue.use(VueRouter)

Vue.use是Vue用来安装第三方插件的函数, 源码在之前的文章# Vue2源码阅读——global-api中讲过,这里不再赘述; 我们就看下 VueRouter是如何安装到Vue上的;


export function install (Vue) {
  // 如果安装过直接返回
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  // 使用 Vue.mixin 
  Vue.mixin({
    beforeCreate () {
      // 我们在 创建 Vue实例的时候, 将 VueRouter的实例 添加到 Vue实例的$options上
      // 这一行只有创建Vue的时候会触发, 因为这个只存在Vue上
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this) // 调用 VueRouter实例的 init 进行初始化
        Vue.util.defineReactive(this, '_route', this._router.history.current) // 定义 _route 为响应式
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this) 
    },
    destroyed () {
      registerInstance(this)
    }
  })

  // 将 VueRouter的实例 添加到 Vue的原型$router上  这就是我们为什么在 Vue组件中 可以 使用 this.$router的关键
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  // 将 上面定义的 _route 添加到 Vue的原型$route上
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 注册全局组件 RouterView 和 RouterLink
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  // 合并策略 采用 strats.created的合并策略, 组成数组 而不是 直接覆盖
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

我们整理一下

  1. 新增混入mixin注册了, beforeCreatedestroyed 两个生命周期函数
  2. VuebeforeCreate生命周期函数中 调用的 VueRouter实例的初始化函数
  3. 新增了一个响应式属性 _route
  4. 在Vue的原型链上 定义了两个只读属性,$router$route, $router 返回的就是 VueRouter实例, $route 返回的就是 前面定义的 响应式的 _route, 这样我们可以在 组件中 watch $route的变化
  5. 注册了 RouterViewRouterLink 两个全局组件
  6. 设置了 beforeRouteEnterbeforeRouteLeavebeforeRouteUpdate(组件内导航守卫) 的合并策略

VueRouter的参数

我们在是示例中提供了moderoutes这两个选项,还有其他选项:

名称类型是否可选说明
routesArray<RouteConfig>路由参数,RouteConfig具体参数查看下表
modestringhash , history, abstract (Node.js 环境), 如何 fallback 为True并且 浏览器不支持 history.pushState,那么自动回退到 hash 模式
fallbackboolean当浏览器不支持 history.pushState控制路由是否应该回退到 hash 模式
basestring应用的基路径。例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 "/app/"
linkActiveClassstring全局配置 <router-link> 默认的激活的 class
linkExactActiveClassstring全局配置 <router-link> 默认的精确激活的class
parseQueryfunction提供自定义查询字符串的解析/反解析函数。覆盖默认行为
stringifyQueryfunction同 parseQuery 只不过返回值是string
scrollBehaviorfunction使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置

RouteConfig

名称类型是否可选说明
pathstring路径
namestring命名路由
componentComponent组件
components{[name: string]:Component}命名视图组件
redirectstring | Location | Function重定向
aliasstring | Array<string>别名
childrenArray<RouteConfig>子路由
beforeEnterfunction路由守卫
metaany元信息
propsboolean | Object | Function属性
caseSensitiveboolean是否忽略大小写
pathToRegexpOptions{sensitive?:boolean, strict?: boolean, end?: boolean}编译正则的选项

new VueRouter

源码文件定位: src/index.js

基本结构

image.png

### 构造函数
```js
constructor (options: RouterOptions = {}) {
    if (process.env.NODE_ENV !== 'production') {
      warn(this instanceof VueRouter, `Router must be called with the new operator.`)
    }
    this.app = null 
    this.apps = []
    this.options = options
    this.beforeHooks = [] // beforeEach 的钩子函数
    this.resolveHooks = [] // beforeResolve 的钩子函数
    this.afterHooks = [] // afterEach 的 钩子函数
    this.matcher = createMatcher(options.routes || [], this) // 创建路由匹配器

    let mode = options.mode || 'hash' // 默认 ‘hash’
    // 如果浏览器不支持 history.pushState是否回退到hash模式
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false 
    if (this.fallback) {
      mode = 'hash'
    }
    // 如果不是浏览器, 那么设置为 abstract 模式
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    
    // 根据同的模式, 创建对应的实例
    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}`)
        }
    }
  }

在初始化的时候, VueRouter 做了以下事情:

  1. routes选项 通过 createMatcher 创建一个 路由匹配器(后序会讲到), 该路由匹配器 返回了 一个对象, 是不是感觉眼熟, 是不是有点肯定了?对, VueRouter 上的 addRoute, getRoutes, addRoutes, match就是 调用的这个匹配器返回的对象
{
  match,
  addRoute,
  getRoutes,
  addRoutes
}
  1. 设置 mode 的值
  2. 根据 mode 初始化对应的 history 实例

init

init (app: any /* Vue component instance */) {

    this.apps.push(app)

    // set up app destroyed handler 
    // 当 app 被销毁时, 将app从当前apps中移除掉,否则会引起 内存泄漏
    // 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
    
    // 对history进行配置 (后面会讲到)
    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
      )
    }

    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }

match

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

currentRoute

获取当前路由

get currentRoute (): ?Route {
  return this.history && this.history.current
}

beforeEach

注册 beforeEach 钩子函数

beforeEach (fn: Function): Function {
  return registerHook(this.beforeHooks, fn)
}

beforeResolve

注册 beforeResolve 钩子函数

beforeResolve (fn: Function): Function {
  return registerHook(this.resolveHooks, fn)
}

afterEach

注册 afterEach 钩子函数

afterEach (fn: Function): Function {
  return registerHook(this.afterHooks, fn)
}

onReady

注册一个回调, 在路由完成初始导航的时候调用

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

onError

注册一个回调, 该回调会在路由导航过程中出错时被调用

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

push

导航到不同的 URL, 这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL;

提示:该方法的参数可以是一个字符串路径,或者一个描述地址的对象

// 字符串
router.push('home')

// 对象
router.push({ path: 'home' })

// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})

// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})

  1. 如果 不传入 onCompleteonAbort,那么返回的 Promise, push('xxx').then(() => {})

  2. 否则采用 回调 push('xxx', () => {}, () => {})

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

replace

replacepush 一样,但是 不会向history插入记录

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

这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步

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

back

这个方法的意思是在 history 记录中后退1步

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

forward

这个方法的意思是在 history 记录中前进1步

forward () {
  this.go(1)
}

getMatchedComponents

返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用,比如:


// category 路由参数
{
  path: '/category',
  name: 'category',
  components: [
    {
      default: component,
      name: {
        template: '<div>NameComponent</div>'
      },
      component: {
        template: '<div>ComponentComponent</div>'
      },
      a: {
        template: '<div>AComponent</div>'
      }
    }
  ]
}
// 获取 /category下的所有的 组件
getMatchedComponents('/category') 
// a: {template: '<div>AComponent</div>'}
// component: {template: '<div>ComponentComponent</div>'}
// default: {template: '<div>12321312</div>'}
// name: {template: '<div>NameComponent</div>'}
getMatchedComponents (to?: RawLocation | Route): Array<any> {
    // 如果 to 不存在 则获取当前的路由 
    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]
        })
      })
    )
  }

resolve

解析目标位置 (格式和 <router-link> 的 to prop 一样)。

  • current 是当前默认的路由 (通常你不需要改变它)
  • append 允许你在 current 路由上附加路径 如同router-link
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
    }
  }

getRoutes

获取所有活跃的路由记录列表

getRoutes () {
    return this.matcher.getRoutes()
  }

addRoute

添加一条新的路由规则记录 或者 作为现有路由的子路由。如果该路由规则有 name,并且已经存在一个与之相同的名字,则会覆盖它。

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

addRoutes

动态添加更多的路由规则, 已废弃 使用 addRoute

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())
    }
  }

可以看到上面的其实 VueRouter 只是起到了 代理的作用,核心的 还是 依靠 historymatcher

  1. 通过history 切换路由
  2. 通过matcher 管理路由和路由组件

Matcher

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // 将 routes 传入到 createRouteMap 生成 pathList, pathMap, nameMap
  const { pathList, pathMap, nameMap } = createRouteMap(routes)

  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

  function addRoute (parentOrRoute, route) {
    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
      )
    }
  }

  function getRoutes () {
    return pathList.map(path => pathMap[path])
  }

  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      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) {
      location.params = {}
      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
    return _createRoute(null, location)
  }

  function redirect (
    record: RouteRecord,
    location: Location
  ): Route {
    const originalRedirect = record.redirect
    let redirect = typeof originalRedirect === 'function'
      ? originalRedirect(createRoute(record, location, null, router))
      : originalRedirect

    if (typeof redirect === 'string') {
      redirect = { path: redirect }
    }

    if (!redirect || typeof redirect !== 'object') {
      if (process.env.NODE_ENV !== 'production') {
        warn(
          false, `invalid redirect option: ${JSON.stringify(redirect)}`
        )
      }
      return _createRoute(null, location)
    }

    const re: Object = redirect
    const { name, path } = re
    let { query, hash, params } = location
    query = re.hasOwnProperty('query') ? re.query : query
    hash = re.hasOwnProperty('hash') ? re.hash : hash
    params = re.hasOwnProperty('params') ? re.params : params

    if (name) {
      // resolved named direct
      const targetRecord = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        assert(targetRecord, `redirect failed: named route "${name}" not found.`)
      }
      return match({
        _normalized: true,
        name,
        query,
        hash,
        params
      }, undefined, location)
    } else if (path) {
      // 1. resolve relative redirect
      const rawPath = resolveRecordPath(path, record)
      // 2. resolve params
      const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
      // 3. rematch with existing query and hash
      return match({
        _normalized: true,
        path: resolvedPath,
        query,
        hash
      }, undefined, location)
    } else {
      if (process.env.NODE_ENV !== 'production') {
        warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
      }
      return _createRoute(null, location)
    }
  }

  function alias (
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {
    const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
    const aliasedMatch = match({
      _normalized: true,
      path: aliasedPath
    })
    if (aliasedMatch) {
      const matched = aliasedMatch.matched
      const aliasedRecord = matched[matched.length - 1]
      location.params = aliasedMatch.params
      return _createRoute(aliasedRecord, location)
    }
    return _createRoute(null, location)
  }

  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)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}
const { pathList, pathMap, nameMap } = createRouteMap(routes)
  1. pathList: 是包含所有路由的路径数组
['', '/news/:id', '/category/news', '/category', '/redirect', '*'] 
  1. pathMap: 是处理过后的路由的对象
{
    '': RouteRecord, 
    '/news/:id': RouteRecord,
    '/category/news': RouteRecord, 
    '/category': RouteRecord, 
    '/redirect': RouteRecord, 
    '*': RouteRecord 
}

// RouteRecord的格式:
{
    path,
    regex, // 经过 path-to-regexp处理后的路由正则
    components, // 路由组件, 如果 route.components存在, 则使用  route.components 否则 将 {default: route.component}
    alias, // 别名
    instances ,
    enteredCbs,
    name,
    parent,
    matchAs,
    redirect,
    beforeEnter,
    meta,
    props
}
  1. nameMap: 所有包含命名路由的对象, 与 pathMap 一样, 只不过 key 值变成了 route.name

addRoute 和 addRoutes

动态添加路由,其实都是调用了。createRouteMap方法

function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

  function addRoute (parentOrRoute, route) {
    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

获取所有路由,pathList 就是上面提到的 路由路径的 数组, 结合 pathMap

function getRoutes () {
  return pathList.map(path => pathMap[path])
}

match

路由匹配

function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    // 标准化路由
    const location = normalizeLocation(raw, currentRoute, false, router)
    // 路由是否包含name,也就是否是是 命名路由
    const { name } = location

    if (name) { 
      const record = nameMap[name] // 获取 路由记录 
      if (!record) return _createRoute(null, location) // 如果不存在 返回空路由 
      // 获取路由的params 比如/:id 或者 /:id?
      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) {
      location.params = {}
      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
    return _createRoute(null, location)
  }

History

因为有三个 不同的模块,我后面会 单独写一篇文章进行解析