VueRouter安裝和初始化導航

VueRouter安裝和初始化導航

想在Vue應用中使用VueRouter, 咱們首先須要安裝VueRouter。 方法以下:vue

安裝VueRouter

Vue.use(VueRouter)

const router = new VueRouter(options)

new Vue({
    el: '#app',
    data: state,
    router, //將路由器對象router做爲一個選項添加到Vue根實例中
    render: h => h(AppLayout),
  })

安裝VueRouter,分爲如下兩步:node

  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方法入手看一下安裝流程react

install 方法

作了如下兩件事:git

  1. 判斷VueRouter是否安裝

    若是已經安裝,退出函數github

  2. 更新Vue對象

    2.1 經過Vue.mixin更新Vue對象的beforeCreatedestroyed 鉤子vue-router

    2.2 經過Object.defineProperty 給Vue對象添加$router 和 $route 屬性。數組

    2.3 在Vue對象上註冊RouterView 和RouterLink 組件app

    2.4 給Vue對象添加路由鉤子函數 beforeRouteEnter, beforeRouteLeave和 beforeRouteUpdate。異步

具體實現以下:async

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 的。
相關文章
相關標籤/搜索