# Vue-Router 中的 install 方法

# Vue 插件 与 Vue.use

Vue 是一个渐进式的 JavaScript 框架,采用了核心库 + 插件的形式。所谓插件其实就是为 Vue 扩展功能的工具,一般来说就是在 Vue 实例上增加了一些属性和方法。

如果要为某个 Vue 实例安装插件,我们需要使用 Vue.use 这个全局 API 去注册插件。Vue.use 接收一个参数,参数可以是对象或者是函数,如果是对象,其必须提供一个 install 方法;如果是函数,则函数本身就会作为这个 install 方法。

我们来看 Vue.use 的源码:

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}
  1. 首先,Vue.use 会对传入的 plugin 进行判重,避免插件重复注册。所以我们如果重复使用 Vue.use 注册插件,是不会多次生效的。
  2. 获取到 Vue 构造函数,也就是代码中的 args。
  3. 进行判断,如果传入的 plugin 是一个函数,则调用此函数,同时将 Vue 构造函数作为参数传入。
  4. 如果传入的 plugin 是一个对象,则调用此对象下的 install 方法,同时将 Vue 构造函数作为参数传入。
  5. 使用 installPlugins 来维护已安装的插件

# Vue-Router 的 install 方法

Vue-Router 中的 install 方法源码:vue-router/src/index.js at v3.1.2 · vuejs/vue-router (github.com) (opens new window)

看似源码较长,实际上大部分代码都在定义 Vue-Router 的相关 API,核心部分代码很少,可以参见本项目的源码。下面仅截取了核心部分代码讲解:

export default class VueRouter {
  static install: () => void;
  static version: string;

  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    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

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

  1. 执行 install 方法时会初始化 VueRouter 对象
  2. 首先初始化 VueRouter 对象的属性和方法,比较重要的属性有 matcher,通过 createMatcher(options.routes || [], this) 创建,matcher 为匹配器,其上定义了 VueRouter 中的动态路由相关 API,并且初始化了 VueRouter 的路由映射表
  3. 其次是根据用户传入的路由模式,来创建不同的 history 对象。history 对象用于监听路由变化以及修改路由等。

# Vue.mixin 在 Vue-Router 中的使用

我们在使用 Vue-Router 时,会传入路由数组,数组中的每一项是一个对象,对应一个路由,对象有 children 属性,对应其子路由数组。我们传入的这个路由数组,是传递给 Vue 根实例的,但是我们希望在该根实例下的每一个子实例中,都可以访问到我们自定义的路由相关信息。所以,我们需要把根实例接收到的路由相关信息共享给其他子实例。

对于这种情况,最先想到的也是最简单的方式,是通过原型链。将路由相关信息添加到 Vue 构造函数的原型链上,这样所有的 Vue 实例都可以通过其构造函数的原型链来获取到路由信息。但是这种方式有一个明显的问题,那就是对应存在多个根实例的情况下,尽管我们只将路由传递给了其中一个根实例,其他根实例依然可以通过构造函数原型的方式访问到路由信息。也就是多个根实例间的路由依然是共享的。

所以,我们采用了 mixin 的方式,而非直接改造原型。

    // 要让所有 Vue 组件都可以访问到路由信息 router
    Vue.mixin({
        beforeCreate() {
            // 将 router 信息保存在 Vue 根实例上,通过共享 Vue 根实例的方式,来共享 router
            if (this.$options.router) {
                // 如果当前实例上的 options 上有 router,说明这一定是 Vue 根实例
                this._routerRoot = this // 保存当前的根实例
                this._router = this.$options.router // 将路由信息保存到根实例上

                // 初始化路由
                this._router.init(this)
            } else {
                // 如果当前组件实例没有 $options.router 属性,那么就将他的父组件的 _routerRoot 值作为该组件的 _routerRoot,以此来实现共享 Vue 根实例
                this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
            }
        }
    })

我们在每个 Vue 实例的 beforeCreate 生命周期中进行判断,如果当前实例为根实例,则将该根实例上新增一个 _router 属性,并且将传入的路由信息挂载到此属性上。并且新增一个 _routerRoot 属性,将根实例(也就是自身)挂载到该属性上。

如果当前实例不是根组件,那么就向其父组件中寻找 _routerRoot 属性,添加为自己的 _routerRoot 属性。

由于 Vue 中,父子组件的加载顺序是先父再子,所以,通过上述这种方式,所有的组件实例上都会挂载 _routerRoot 属性,这个属性的值就是 Vue 根组件,通过根组件的 _router 属性即可找到对应的路由信息。

这种方式避免了不同根组件间路由信息的相互共享。我们通过同一子组件下共享根实例的方式,保证了根实例的路由信息只会在其后代组件中共享。同时,修改时只需要修改根组件的路由信息即可,而不用修改每一个组件的路由信息。

这样做有一个缺点,就是路由信息的访问十分麻烦,需要通过 this._routerRoot.router 的方式,先通过根组件再获取路由信息。为了用户的使用方便,使用 defineProperty 做了一层代理:

    Object.defineProperty(Vue.prototype, '$router', {
        get() {
            // 代理一层,不通过 this._routerRoot.router 来访问路由,而是通过 this.$router 的便捷方式
            return this._routerRoot && this._routerRoot._router
        }
    })