zoukankan      html  css  js  c++  java
  • 前端路由的实现(一)

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

    • 利用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()

      

  • 相关阅读:
    记一次Cloudera中页面监控失效问题
    spark on yarn 动态资源分配报错的解决:org.apache.hadoop.yarn.exceptions.InvalidAuxServiceException: The auxService:spark_shuffle does not exist
    sparkOnYarn报错org.apache.hadoop.fs.FSDataInputStream
    记一次newApiHadoopRdd查询数据不一致问题
    记一次sparkOnyarn错误:java.lang.UnsatisfiedLinkError
    线程八大基础核心二(启动线程)
    线程八大基础核心一(创建线程的方式)
    非常时期,做好一个普通人
    哈希算法(应用场景)
    查找算法(二分查找)
  • 原文地址:https://www.cnblogs.com/xuzhudong/p/8869699.html
Copyright © 2011-2022 走看看