前端路由

”更新视图但不重新请求页面“是前端路由的核心,浏览器中主要有两种方式实现这种功能:

路由控制

1)hash

angular和vue目前实现的路由,总会在path后面加上#!这些,兼容性很好,ie6也是支持的。

  hash虽然出现在URL中,但不会被包含在HTTP请求中,改变hash不会重新加载页面;

  每一次改变hash都会在浏览器的访问历史中添加一个记录;

  可以为hash的改变添加监听事件;

2)history

history是html5新添加的api,history的路由优势在于美观,缺点是兼容问题

 

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); breakcase 'hash':
      this.history = new HashHistory(this, options.base, this.fallback);breakcase '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);
};
View Code

 

  mode作为参数传入,用来指示实际起作用的对象属性history的实现类。

  对mode做校验,默认为’hash‘;若浏览器不支持HTML5History方式,则强制设为’hash‘;若不是浏览器环境运行,设为’abstract‘;根据mode初始化对应的history。

  VueRouter类中的onReady(), push()等方法实际是调用具体history对象对应的方法。

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;
  }
}
View Code

 

  push方法里面有个transitionTo方法,它是父类定义用来处理路由变化中的基础逻辑;而transitionTo方法其中的pushHash方法主要对window的hash进行赋值;

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);
  });
};
View Code

 

  父类的transitionTo方法调用了updateRoute,调用了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;
    });
  });
};
View Code

 

  app为Vue组件的实例,本身并没有关于内置属性_route,是在VueRouter的install()方法中混合入Vue对象的。

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;
}
View Code

 

  该混合在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的_route属性。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。

总结一下,从设置路由改变到视图更新的流程如下:

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

原文链接:https://www.cnblogs.com/xuzhudong/p/8869699.html  

 

posted @ 2020-03-13 10:24  Lorasysu  阅读(88)  评论(0)    收藏  举报