前端路由的實現(一)


“更新視圖但不重新請求頁面”是前端路由原理的核心之一,目前在瀏覽器環境中這一功能的實現主要有兩種方式:

  • 利用URL中的hash(“#”)

  • 利用History interface在 HTML5中新增的方法

vue-router是Vue.js框架的路由插件,下面我們從它的源碼入手,邊看代碼邊看原理,由淺入深觀摩vue-router是如何通過這兩種方式實現前端路由的。

模式參數

在vue-router中是通過mode這一參數控制路由的實現模式的:

const router = new VueRouter({
 mode: 'history',
 routes: [...]
})

vue-router的實際源碼如下:

export default class VueRouter {
 
 mode: string; // 傳入的字符串參數,指示history類別
 history: HashHistory | HTML5History | AbstractHistory; // 實際起作用的對象屬性,必須是以上三個類的枚舉
 fallback: boolean; // 如瀏覽器不支持,'history'模式需回滾為'hash'模式
 
 constructor (options: RouterOptions = {}) {
 
 let mode = options.mode || 'hash' // 默認為'hash'模式
 this.fallback = mode === 'history' && !supportsPushState // 通過supportsPushState判斷瀏覽器是否支持'history'模式
 if (this.fallback) {
     mode = 'hash'
 }
 if (!inBrowser) {
     mode = 'abstract' // 不在瀏覽器環境下運行需強制為'abstract'模式
 }
 this.mode = mode

 // 根據mode確定history實際的類並實例化
 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: $`)
      }
    }
 }

 init (app: any /* Vue component instance */) {
 const history = this.history;
 // 根據history的類別執行相應的初始化操作和監聽
 if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
 } else if (history instanceof HashHistory) {
     const setupHashListener = () => {
     history.setupListeners()
 }
 history.transitionTo(
     history.getCurrentLocation(),
     setupHashListener,
     setupHashListener
   )
 }

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

 // VueRouter類暴露的以下方法實際是調用具體history對象的方法

VueRouter.prototype.beforeEach = function beforeEach (fn) {
     return registerHook(this.beforeHooks, fn)
};

VueRouter.prototype.beforeResolve = function beforeResolve (fn) {
    return registerHook(this.resolveHooks, fn)
};

VueRouter.prototype.afterEach = function afterEach (fn) {
    return registerHook(this.afterHooks, fn)
};

VueRouter.prototype.onReady = function onReady (cb, errorCb) {
    this.history.onReady(cb, errorCb);
};

VueRouter.prototype.onError = function onError (errorCb) {
    this.history.onError(errorCb);
};

VueRouter.prototype.push = function push (location, onComplete, onAbort) {
    this.history.push(location, onComplete, onAbort);
};

VueRouter.prototype.replace = function replace (location, onComplete, onAbort) {
    this.history.replace(location, onComplete, onAbort);
};

VueRouter.prototype.go = function go (n) {
   this.history.go(n);
};

VueRouter.prototype.back = function back () {
   this.go(-1);
};

VueRouter.prototype.forward = function forward () {
    this.go(1);
};

 從上面代碼可以看出:

  1. 作為參數傳入的字符串屬性mode只是一個標記,用來指示實際起作用的對象屬性history的實現類,兩者對應關系如下:

       mode的對應如右——history:'HTML5History;'hash':'HashHistory'; abstract:'AbstractHistory';
  2. 在初始化對應的history之前,會對mode做一些校驗:若瀏覽器不支持HTML5History方式(通過supportsPushState變量判斷),則mode強制設為'hash';若不是在瀏覽器環境下運行,則mode強制設為'abstract'

  3. VueRouter類中的onReady(), push()等方法只是一個代理,實際是調用的具體history對象的對應方法,在init()方法中初始化時,也是根據history對象具體的類別執行不同操作

     在瀏覽器環境下的兩種方式,分別就是在HTML5History,HashHistory兩個類中實現的。History中定義的是公用和基礎的方法,直接看會一頭霧水,我們先從HTML5History,HashHistory兩個類中看着親切的push(), replace()方法的說起。

    HashHistory

hash(“#”)符號的本來作用是加在URL中指示網頁中的位置:

http://www.example.com/index.html#print

 

     #符號本身以及它后面的字符稱之為hash,可通過window.location.hash屬性讀取。它具有如下特點:

  • hash雖然出現在URL中,但不會被包括在HTTP請求中。它是用來指導瀏覽器動作的,對服務器端完全無用,因此,改變hash不會重新加載頁面

  • 可以為hash的改變添加監聽事件:

    window.addEventListener("hashchange", funcRef, false) 
  • 每一次改變hash(window.location.hash),都會在瀏覽器的訪問歷史中增加一個記錄

利用hash的以上特點,就可以來實現前端路由“更新視圖但不重新請求頁面”的功能了。

 

頁面URL如下:

 

 

輸出如下: 

 

HashHistory.push()

我們來看HashHistory中的push()方法:

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

    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
      pushHash(route.fullPath);
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    }, onAbort);
  };
// 對window的hash進行直接賦值
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path));
  } else {
    window.location.hash = path;
  }
}

transitionTo()方法是父類中定義的是用來處理路由變化中的基礎邏輯的,push()方法最主要的是對window的hash進行了直接賦值:

 pushHash(route.fullPath);

hash的改變自動添加到瀏覽器的訪問歷史記錄中。

那么視圖的更新是怎么實現的呢,我們來看父類History中transitionTo()方法的這么一段:

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) {
      onAbort(err);
    }
    if (err && !this$1.ready) {
      this$1.ready = true;
      this$1.readyErrorCbs.forEach(function (cb) { cb(err); });
    }
  });
};

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

  var current = this.current;
  var abort = function (err) {
    if (isError(err)) {
      if (this$1.errorCbs.length) {
        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()
  }

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

  var queue = [].concat(
    // in-component leave guards
    extractLeaveGuards(deactivated),
    // global before hooks
    this.router.beforeHooks,
    // in-component update hooks
    extractUpdateHooks(updated),
    // in-config enter guards
    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 () {
    var postEnterCbs = [];
    var isValid = function () { return this$1.current === route; };
    // wait until async components are resolved before
    // extracting in-component enter guards
    var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
    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);
      if (this$1.router.app) {
        this$1.router.app.$nextTick(function () {
          postEnterCbs.forEach(function (cb) { cb(); });
        });
      }
    });
  });
};

History.prototype.updateRoute = function updateRoute (route) {
  var prev = this.current;
  this.current = route;
  this.cb && this.cb(route);
  this.router.afterHooks.forEach(function (hook) {
    hook && hook(route, prev);
  });
};

可以看到,當路由變化時,調用了History中的this.cb方法,而this.cb方法是通過History.listen(cb)進行設置的。回到VueRouter類定義中,找到了在init()方法中對其進行了設置:

VueRouter.prototype.init = function init (app /* Vue component instance */) {
    var this$1 = this;

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

  // main app already initialized.
  if (this.app) {
    return
  }

  this.app = app;

  var history = this.history;

  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation());
  } else if (history instanceof HashHistory) {
    var setupHashListener = function () {
      history.setupListeners();
    };
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    );
  }

  history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
  });
};

根據注釋,app為Vue組件實例,但我們知道Vue作為漸進式的前端框架,本身的組件定義中應該是沒有有關路由內置屬性_route,如果組件中要有這個屬性,應該是在插件加載的地方,即VueRouter的install()方法中混合入Vue對象的,查看install.js源碼,有如下一段:

function install (Vue) {
  if (install.installed && _Vue === Vue) { return }
  install.installed = true;

  _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.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.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);
    },
    destroyed: function destroyed () {
      registerInstance(this);
    }
  });

  Object.defineProperty(Vue.prototype, '$router', {
    get: function get () { return this._routerRoot._router }
  });

  Object.defineProperty(Vue.prototype, '$route', {
    get: function get () { return this._routerRoot._route }
  });

  Vue.component('router-view', View);
  Vue.component('router-link', Link);

  var strats = Vue.config.optionMergeStrategies;
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

通過Vue.mixin()方法,全局注冊一個混合,影響注冊之后所有創建的每個 Vue 實例,該混合在beforeCreate鈎子中通過Vue.util.defineReactive()定義了響應式的_route屬性。所謂響應式屬性,即當_route值改變時,會自動調用Vue實例的render()方法,更新視圖。

總結一下,從設置路由改變到視圖更新的流程如下:

$router.push() -->; HashHistory.push() -->; History.transitionTo() -->; History.updateRoute() -->; vm.render()

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM