乐趣区

VueRouter安装和初始化导航

VueRouter 安装和初始化导航

想在 Vue 应用中使用 VueRouter, 我们首先需要安装 VueRouter。方法如下:

安装 VueRouter

Vue.use(VueRouter)

const router = new VueRouter(options)

new Vue({
    el: '#app',
    data: state,
    router, // 将路由器对象 router 作为一个选项添加到 Vue 根实例中
    render: h => h(AppLayout),
  })

安装 VueRouter,分为以下两步:

  1. 使用 Vue.use 通过调用 VueRouter 插件中的 install 方法把 VueRouter 安装到 Vue
  2. 为了每个 Vue 实例都能访问 VueRouter 实例,创建 Vue 根实例时,把路由器对象 router 注入到 Vue 实例

使用 Vue.use 把 VueRouter 安装到 Vue

Vue.use 通过调用 VueRouter 插件中的 install 方法把 VueRouter 安装到 Vue。我们从 VueRouter 的 install 方法入手看一下安装流程

install 方法

做了以下两件事:

  1. 判断 VueRouter 是否安装

    如果已经安装,退出函数

  2. 更新 Vue 对象

    2.1 通过 Vue.mixin 更新 Vue 对象的 beforeCreatedestroyed 钩子

    2.2 通过 Object.defineProperty 给 Vue 对象添加 $router 和 $route 属性。

    2.3 在 Vue 对象上注册 RouterView 和 RouterLink 组件

    2.4 给 Vue 对象添加路由钩子函数 beforeRouteEnter,beforeRouteLeave 和 beforeRouteUpdate。

具体实现如下:

var _Vue;

function install(Vue) {if (install.installed && _Vue === Vue) {return} // 已安装过,退出函数
    install.installed = true; // installed 标记已安装

    // 保存 Vue,同时用于检测是否重复安装
    _Vue = Vue;

    // 
    var isDef = function (v) {return v !== undefined;};

    var registerInstance = function (vm, callVal) {
        var i = vm.$options._parentVnode;
        if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal);
        }
    };
    // 更新 Vue 对象中的 beforeCreate 和 destroyed 钩子
    Vue.mixin({beforeCreate: function beforeCreate() {if (isDef(this.$options.router)) { // 调用 Vue 构造函数创建实例时传入了 router 选项(根组件)this._routerRoot = this;
                this._router = this.$options.router;
                this._router.init(this);
                Vue.util.defineReactive(this, '_route', this._router.history.current);
            } else {this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
            }
            registerInstance(this, this); // 把 RouterView 添加到虚拟 Node
        },
        destroyed: function destroyed() {registerInstance(this);// 从虚拟 node 中移除 RouterView
        }
    });
    // 给 Vue 对象添加 $router 属性
    Object.defineProperty(Vue.prototype, '$router', {get: function get() {return this._routerRoot._router}
    });
    // 给 Vue 对象添加 $route 属性
    Object.defineProperty(Vue.prototype, '$route', {get: function get() {return this._routerRoot._route}
    });

    Vue.component('RouterView', View); // 注册 RouterView 组件,对应模板中的 <router-view />
    Vue.component('RouterLink', Link);// 注册 RouterLink 组件,对应模板中的 <router-link />

    // 给 Vue 对象添加路由钩子函数 beforeRouteEnter,beforeRouteLeave,beforeRouteUpdate
    var strats = Vue.config.optionMergeStrategies;
    // use the same hook merging strategy for route hooks
    strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

创建 Vue 根实例时,把路由器对象 router 注入到 Vue 实例

new Vue({
    el: '#app',
    data: state,
    router, // 将路由器对象 router 作为一个选项添加到 Vue 根实例中
    render: h => h(AppLayout),
  })

由于 调用 install 方法时,更新了 Vue 对象构造器,那么调用 new Vue(options) 构建 Vue 根实例时,

​ 每个 Vue 实例的初始化阶段都会在 beforeCreate 钩子中更新路由相关的操作,销毁阶段在destroyed 钩子中销毁路由相关的虚拟节点

​ 每个 Vue 实例中都有 $router 和 $route 属性。每个 Vue 实例都可以通过 this.$router 访问全局的 VueRouter 实例,通过 this.$route 访问当前路由记录。

​ 每个 Vue 实例都能使用 RouterView 和 RouterLink 组件

​ 每个 Vue 实例包含路由钩子函数,可用于 组件内 路由导航守卫。

Vue 实例的初始化阶段在 beforeCreate 钩子做了哪些路由相关的操作?
  • ​ 给 Vue 实例添加_routerRoot 属性,用于 $router 属性 和 $route 属性从中读取值

​ 如果当前 Vue 实例是根实例

​ 那么给当前 Vue 实例添加_routerRoot 属性,_router 属性 和 _route 属性

​ 由于_routerRoot 指向当前 Vue 实例,那么 _routerRoot 中就包含了 _router 属性 和 _route 属性。

​ 如果不是根实例,

​ 那么给当前 Vue 实例添加_routerRoot 属性,_routerRoot 是从父实例中获取的。

​ 这样一来所有 Vue 实例就都有了_routerRoot 属性。所有 Vue 实例 _routerRoot 属性都是从相同的地方获取值。

  • ​ 把 RouterView 组件需要展示的视图对应的虚拟节点添加到虚拟节点树
Vue 实例的销毁阶段在destroyed 钩子做了哪些路由相关的操作?
  • 把 RouterView 组件需要展示的视图对应的虚拟节点从虚拟节点树移除
Vue.mixin({beforeCreate: function beforeCreate() { // 混淆进 Vue 的 beforeCreacte 钩子中
      if (isDef(this.$options.router)) { // 调用 Vue 构造函数创建实例时传入了 router 选项(根组件)this._routerRoot = this;// this._routerRoot 指向当前 Vue 实例
          this._router = this.$options.router; 
          this._router.init(this); // 调用 VueRouter 实例的 init 方法进行初始化导航
          // 使 Vue 根实例中的_route 响应式
          //history.current 记录当前路由对象信息
          Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this); // 把 RouterView 组件需要展示的视图对应的虚拟节点添加到虚拟节点树
  },
  destroyed: function destroyed() { // 混淆进 Vue 的 destroyed 钩子中
      registerInstance(this); // 把 RouterView 组件需要展示的视图对应的虚拟节点从虚拟节点树移除
  }
});

Vue 实例的 $router 属性 和 $route 属性都从 Vue 实例的_routerRoot 属性中获取值。

// 给 Vue 对象添加 $router 属性
Object.defineProperty(Vue.prototype, '$router', {get: function get() {return this._routerRoot._router}
});
// 给 Vue 对象添加 $route 属性
Object.defineProperty(Vue.prototype, '$route', {get: function get() {return this._routerRoot._route}
});

下面我们看一下如何调用 VueRouter 实例的 init 方法进行初始化导航。

VueRouter 的 init 方法

做了以下几件事:

  1. 检查 VueRouter 安装
  2. 更新 VueRouter 实例 apps 属性 和 app 属性

    ​ 在 VueRouter 实例的 apps 属性中记录调用当前 VueRouter 实例的所有 Vue 实例。

    ​ 在 VueRouter 实例的 app 属性中记录调用当前 VueRouter 实例的 Vue 实例。

  3. 根据路由的当前路径进行导航

    history.getCurrentLocation()获取当前路径 eg : ‘/login’

  4. 监听 VueRouter 实例中记录会话历史的 history 对象。如果发生变化,更新 apps 数组中每个 Vue 实例的_route 属性。
VueRouter.prototype.init = function init(app /* Vue component instance */) {
    var this$1 = this;

    assert(
        install.installed,
        "not installed. Make sure to call `Vue.use(VueRouter)`" +
        "before creating root instance."
    ); //1. 检查 VueRouter 安装

    this.apps.push(app);//  2. 把当前 Vue 实例添加到 VueRouter 实例的 apps 属性中

    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    app.$once('hook:destroyed', function () {
        // clean out app from this.apps array once destroyed
        // 如果当前 Vue 实例调用了 destroyed 钩子函数。那么从 VueRouter 实例的 apps 属性中删除当前 Vue 实例
        var index = this$1.apps.indexOf(app);
        if (index > -1) {this$1.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$1.app === app) {this$1.app = this$1.apps[0] || null; }
    });

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

    this.app = app; // 更新 VueRouter 实例中的 app 属性指向当前 Vue 实例

    var history = this.history; // 根据 router 的 mode 不同,HTML5History 对象,HashHistory 对象,AbstractHistory 对象

    if (history instanceof HTML5History) { // history 是 HTML5History 对象, mode == 'history'
        //history.getCurrentLocation() 获取当前路径 '/'
        history.transitionTo(history.getCurrentLocation()); // 导航到当前路径对应的页面
    } else if (history instanceof HashHistory) { // history 是 HashHistory 对象, mode == 'hash'
        var setupHashListener = function () {history.setupListeners();
        };
        history.transitionTo(history.getCurrentLocation(),
            setupHashListener,
            setupHashListener
        );
    }

    // 监听记录会话历史的 history 对象。如果路由发生变化,更新 apps 数组中每个 Vue 实例的_route 属性
    history.listen(function (route) {this$1.apps.forEach(function (app) {app._route = route;});
    });
};

通过代码可以看到 VueRouter 中实现导航的使用 transitionTo 方法。

transitionTo

作用:

​ 导航到期望的路由

参数:

​ location: 是一个字符串路径

​ onComplete:一个回调函数,在确认导航后执行

​ onAbort:一个回调函数,在终止导航解析 (导航到相同的路由,或在当前导航完成之前导航到另一个不同的路由) 时执行

工作流程:

  1. 根据参数 location 获取目的路由对象 route
  2. 调用confirmTransition 确认导航

    2.1 如果确认要导航到目的路由,执行以下操作

    ​ a) 调用 updateRoute 更新路由

    ​ b) 如果存在 onComplete,调用 onComplete 回调函数

    ​ c) 调用 ensureURL 进行导航

    ​ d) 如果还没有执行需要在 完成初始化导航 后需要调用的回调函数,那么就执行回调,这些回调函数通过 onReady 注册,保存在 readyCbs 中

    2.2 如果终止导航解析,执行以下操作

    ​ a) 如果存在 onAbort,调用 onAbort 回调函数

    ​ b) 如果还没有执行 初始化路由解析运行出错 时需要调用的回调函数,那么就执行回调。这些回调函数通过 onReady 定义,保存在 readyErrorCbs 中

具体实现如下:

History.prototype.transitionTo = function transitionTo(
    location,
    onComplete,
    onAbort
) {
    var this$1 = this;

    // 获取目的路由对象
    var route = this.router.match(location, this.current);
    this.confirmTransition( // 确认导航
        route,
        function () { // 进行导航
            this$1.updateRoute(route); // 更新路由
            onComplete && onComplete(route);
            this$1.ensureURL();// 进行导航

            // fire ready cbs once
            if (!this$1.ready) {// 没有执行需要在完成初始化导航后需要调用的回调函数
                this$1.ready = true;
                this$1.readyCbs.forEach(function (cb) {cb(route);
                });
            }
        },
        function (err) { // 终止导航
            if (onAbort) { // 如果调用 transitionTo 传入的参数中有回调函数
                onAbort(err);
            }
            if (err && !this$1.ready) {// 没有执行初始化路由解析运行出错时需要调用的回调函数
                this$1.ready = true;
                this$1.readyErrorCbs.forEach(function (cb) {cb(err);
                });
            }
        }
    );
};
调用 confirmTransition 确认导航

confirmTransition

作用:

​ 确认导航

参数:

​ route: 要确认的路由对象

​ onComplete:一个回调函数,在确认导航后执行

​ onAbort:一个回调函数,在终止导航解析 (导航到相同的路由,或在当前导航完成之前导航到另一个不同的路由) 时执行

工作原理:

要解析的目的路由对象和当前路由对象是否相同,

  • 如果相同

    调用 ensureURL 直接导航到当前路由

    调用 abort 来停止导航解析流程

  • 如果不同,按照以下顺序解析导航
  1. 在失活的组件里调用离开路由守卫beforeRouteLeave
  2. 调用全局前置守卫beforeEach
  3. 在重用的组件里调用更新路由守卫 beforeRouteUpdate
  4. 在路由配置里调用beforeEnter
  5. 解析 异步路由组件
  6. 在被激活的组件里调用beforeRouteEnter
  7. 调用全局解析守卫beforeResolve
  8. 导航被确认
  9. 调用 updateRoute 更新路由

    调用全局后置守卫afterEach

  10. 调用 ensureURL 进行导航
  11. 触发 DOM 更新
  12. 在 DOM 更新之后调用回调函数,这些回调函数是通过 beforeRouteEnter 守卫传给 next 的。

具体实现如下:

History.prototype.confirmTransition = function confirmTransition(route, onComplete, onAbort) {
    var this$1 = this;

    var current = this.current;
    var abort = function (err) {
        // after merging https://github.com/vuejs/vue-router/pull/2771 we
        // When the user navigates through history through back/forward buttons
        // we do not want to throw the error. We only throw it if directly calling
        // push/replace. That's why it's not included in isError
        if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {if (this$1.errorCbs.length) { // this$1.errorCbs 是通过 onError 注册的回调
                this$1.errorCbs.forEach(function (cb) {cb(err);
                });
            } else {warn(false, 'uncaught error during route navigation:');
                console.error(err);
            }
        }
        onAbort && onAbort(err);
    };
    // 如果目的路由对象和当前路由对象相同 -- 导航重复
    if (isSameRoute(route, current) &&
        // in the case the route map has been dynamically appended to
        route.matched.length === current.matched.length
    ) {
        // 那么直接导航到当前路由
        this.ensureURL();
        return abort(new NavigationDuplicated(route))// 停止导航解析流程
    }

    var ref = resolveQueue(
        this.current.matched,
        route.matched
    );
    var updated = ref.updated;
    var deactivated = ref.deactivated;
    var activated = ref.activated;

    // 把守卫钩子存在 queue 中,按顺序执行
    var queue = [].concat(
        // in-component leave guards -- 在失活的组件里调用离开路由守卫 beforeRouteLeave
        extractLeaveGuards(deactivated),
        // global before hooks -- 调用全局前置守卫 beforeEach
        this.router.beforeHooks,
        // in-component update hooks -- 在重用的组件里调用更新路由守卫 beforeRouteUpdate
        extractUpdateHooks(updated),
        // in-config enter guards -- 在路由配置里调用 beforeEnter
        activated.map(function (m) {return m.beforeEnter;}),
        // async components -- 解析异步组件
        resolveAsyncComponents(activated)
    );

    this.pending = route;
    var iterator = function (hook, next) {if (this$1.pending !== route) {return abort()
        }
        try {hook(route, current, function (to) {if (to === false || isError(to)) {// next(false) -> abort navigation, ensure current URL
                    this$1.ensureURL(true);
                    abort(to);
                } else if (
                    typeof to === 'string' ||
                    (typeof to === 'object' &&
                        (typeof to.path === 'string' || typeof to.name === 'string'))
                ) {// next('/') or next({path: '/'}) -> redirect 
                    abort();
                    if (typeof to === 'object' && to.replace) {this$1.replace(to);
                    } else {this$1.push(to);
                    }
                } else {
                    // confirm transition and pass on the value
                    next(to);
                }
            });
        } catch (e) {abort(e);
        }
    };

    runQueue(queue, iterator, function () {// 执行完 queue 中的钩子函数后
        var postEnterCbs = [];
        var isValid = function () { return this$1.current === route;};
        // wait until async components are resolved before
        // extracting in-component enter guards -- 在被激活的组件里调用 beforeRouteEnter
        var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
        //this$1.router.resolveHooks 调用全局解析守卫 beforeResolve
        var queue = enterGuards.concat(this$1.router.resolveHooks);
        runQueue(queue, iterator, function () {if (this$1.pending !== route) {return abort() // 停止导航解析流程
            }
            this$1.pending = null;
            onComplete(route); // 导航被确认,调用在 transitionTo 中定义的确认后的回调函数
            if (this$1.router.app) {this$1.router.app.$nextTick(function () {
                    // 在 DOM 更新之后调用回调函数,这些回调函数是通过 beforeRouteEnter 守卫传给 next 的
                    postEnterCbs.forEach(function (cb) {cb();
                    });
                });
            }
        });
    });
};
调用 updateRoute 更新路由

更新路由,并调用全局后置守卫 afterEach

History.prototype.updateRoute = function updateRoute(route) {
  var prev = this.current;
  // 更新保存当前路由记录的变量 this.current
  this.current = route;
  this.cb && this.cb(route);
  // 调用全局后置守卫 afterEach
  this.router.afterHooks.forEach(function (hook) {hook && hook(route, prev);
  });
};

完整导航解析流程

  1. 导航被触发,进入transitionTo 函数
  2. 获取目的路由对象 route
  3. 调用confirmTransition 确认导航目的路由对象 route
  4. 在失活的组件里调用离开路由守卫beforeRouteLeave
  5. 调用全局前置守卫beforeEach
  6. 在重用的组件里调用更新路由守卫 beforeRouteUpdate
  7. 在路由配置里调用beforeEnter
  8. 解析 异步路由组件
  9. 在被激活的组件里调用beforeRouteEnter
  10. 调用全局解析守卫beforeResolve
  11. 确认要导航到目的路由
  12. 调用 updateRoute 更新路由

    调用全局后置守卫afterEach

  13. 调用 ensureURL 进行导航
  14. 触发 DOM 更新
  15. 在 DOM 更新之后调用回调函数,这些回调函数是通过 beforeRouteEnter 守卫传给 next 的。
退出移动版