美文网首页
四、History

四、History

作者: zdxhxh | 来源:发表于2020-02-02 20:23 被阅读0次

    接着上一节讲到,router对象最重要的属性是history属性。最常见的History有两种模式。

    • hash
    • history

    两种都大同小异,我们来看history模式的类——History

    History

    export class History {
      router: Router;  // router引用
      base: string;  // 基础路径
      current: Route;  // 当前路由对象
      pending: ?Route;  
      cb: (r: Route) => void;  // 存放订阅方法
      ready: boolean;
      readyCbs: Array<Function>;
      readyErrorCbs: Array<Function>;
      errorCbs: Array<Function>;
    
      // 抽象方法 子类必须实现
      +go: (n: number) => void;
      +push: (loc: RawLocation) => void;
      +replace: (loc: RawLocation) => void;
      +ensureURL: (push?: boolean) => void;
      +getCurrentLocation: () => string;
    
      constructor (router: Router, base: ?string) {
        this.router = router
        this.base = normalizeBase(base)
        // start with a route object that stands for "nowhere"
        this.current = START
        this.pending = null
        this.ready = false
        this.readyCbs = []
        this.readyErrorCbs = []
        this.errorCbs = []
      }
      // History发布的订阅处理接口
      listen (cb: Function) {
        this.cb = cb
      }
    
      onReady (cb: Function, errorCb: ?Function) {
        if (this.ready) {
          cb()
        } else {
          this.readyCbs.push(cb)
          if (errorCb) {
            this.readyErrorCbs.push(errorCb)
          }
        }
      }
    
      onError (errorCb: Function) {
        this.errorCbs.push(errorCb)
      }
      // 当路由改变时 会触发该方法
      transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const route = this.router.match(location, this.current)
        this.confirmTransition(route, () => {
          this.updateRoute(route)
          onComplete && onComplete(route)
          this.ensureURL()
    
          // fire ready cbs once
          if (!this.ready) {
            this.ready = true
            this.readyCbs.forEach(cb => { cb(route) })
          }
        }, err => {
          if (onAbort) {
            onAbort(err)
          }
          if (err && !this.ready) {
            this.ready = true
            this.readyErrorCbs.forEach(cb => { cb(err) })
          }
        })
      }
    
      confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
        const current = this.current
        const abort = err => {
          if (isError(err)) {
            if (this.errorCbs.length) {
              this.errorCbs.forEach(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()
        }
    
        const {
          updated,
          deactivated,
          activated
        } = resolveQueue(this.current.matched, route.matched)
    
        const queue: Array<?NavigationGuard> = [].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(m => m.beforeEnter),
          // async components
          resolveAsyncComponents(activated)
        )
    
        this.pending = route
        const iterator = (hook: NavigationGuard, next) => {
          if (this.pending !== route) {
            return abort()
          }
          try {
            hook(route, current, (to: any) => {
              if (to === false || isError(to)) {
                // next(false) -> abort navigation, ensure current URL
                this.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.replace(to)
                } else {
                  this.push(to)
                }
              } else {
                // confirm transition and pass on the value
                next(to)
              }
            })
          } catch (e) {
            abort(e)
          }
        }
    
        runQueue(queue, iterator, () => {
          const postEnterCbs = []
          const isValid = () => this.current === route
          // wait until async components are resolved before
          // extracting in-component enter guards
          const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
          const queue = enterGuards.concat(this.router.resolveHooks)
          runQueue(queue, iterator, () => {
            if (this.pending !== route) {
              return abort()
            }
            this.pending = null
            onComplete(route)
            if (this.router.app) {
              this.router.app.$nextTick(() => {
                postEnterCbs.forEach(cb => { cb() })
              })
            }
          })
        })
      }
      // 触发订阅方法,改变_route.current,从而更新视图
      updateRoute (route: Route) {
        const prev = this.current
        this.current = route
        this.cb && this.cb(route)
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })
      }
    }
    
    function normalizeBase (base: ?string): string {
      if (!base) {
        if (inBrowser) {
          // respect <base> tag
          const baseEl = document.querySelector('base')
          base = (baseEl && baseEl.getAttribute('href')) || '/'
          // strip full URL origin
          base = base.replace(/^https?:\/\/[^\/]+/, '')
        } else {
          base = '/'
        }
      }
      // make sure there's the starting slash
      if (base.charAt(0) !== '/') {
        base = '/' + base
      }
      // remove trailing slash
      return base.replace(/\/$/, '')
    }
    
    function resolveQueue (
      current: Array<RouteRecord>,
      next: Array<RouteRecord>
    ): {
      updated: Array<RouteRecord>,
      activated: Array<RouteRecord>,
      deactivated: Array<RouteRecord>
    } {
      let i
      const max = Math.max(current.length, next.length)
      for (i = 0; i < max; i++) {
        if (current[i] !== next[i]) {
          break
        }
      }
      return {
        updated: next.slice(0, i),
        activated: next.slice(i),
        deactivated: current.slice(i)
      }
    }
    
    function extractGuards (
      records: Array<RouteRecord>,
      name: string,
      bind: Function,
      reverse?: boolean
    ): Array<?Function> {
      const guards = flatMapComponents(records, (def, instance, match, key) => {
        const guard = extractGuard(def, name)
        if (guard) {
          return Array.isArray(guard)
            ? guard.map(guard => bind(guard, instance, match, key))
            : bind(guard, instance, match, key)
        }
      })
      return flatten(reverse ? guards.reverse() : guards)
    }
    
    function extractGuard (
      def: Object | Function,
      key: string
    ): NavigationGuard | Array<NavigationGuard> {
      if (typeof def !== 'function') {
        // extend now so that global mixins are applied.
        def = _Vue.extend(def)
      }
      return def.options[key]
    }
    
    function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
      return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
    }
    
    function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
      return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
    }
    
    function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
      if (instance) {
        return function boundRouteGuard () {
          return guard.apply(instance, arguments)
        }
      }
    }
    
    function extractEnterGuards (
      activated: Array<RouteRecord>,
      cbs: Array<Function>,
      isValid: () => boolean
    ): Array<?Function> {
      return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
        return bindEnterGuard(guard, match, key, cbs, isValid)
      })
    }
    
    function bindEnterGuard (
      guard: NavigationGuard,
      match: RouteRecord,
      key: string,
      cbs: Array<Function>,
      isValid: () => boolean
    ): NavigationGuard {
      return function routeEnterGuard (to, from, next) {
        return guard(to, from, cb => {
          next(cb)
          if (typeof cb === 'function') {
            cbs.push(() => {
              // #750
              // if a router-view is wrapped with an out-in transition,
              // the instance may not have been registered at this time.
              // we will need to poll for registration until current route
              // is no longer valid.
              poll(cb, match.instances, key, isValid)
            })
          }
        })
      }
    }
    
    function poll (
      cb: any, // somehow flow cannot infer this is a function
      instances: Object,
      key: string,
      isValid: () => boolean
    ) {
      if (instances[key]) {
        cb(instances[key])
      } else if (isValid()) {
        setTimeout(() => {
          poll(cb, instances, key, isValid)
        }, 16)
      }
    }
    
    

    HTML5History

    描述 : 如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

    export class HTML5History extends History {
      constructor(router: Router, base: ?string) {
        // 继承router 对象 
        // base : 基础路径
        super(router, base);   
        // 额外滚动事件
        const expectScroll = router.options.scrollBehavior;
        // 是否支持history的api 并且有滚动事件
        const supportsScroll = supportsPushState && expectScroll;
        // 如果满足上述条件
        if (supportsScroll) {
          // 记录滚动位置
          setupScroll();
        }
        // 获取location对象
        const initLocation = getLocation(this.base);
        // 监听popstate方法
        window.addEventListener("popstate", e => {
          // 获取current对象
          const current = this.current;
    
          // Avoiding first `popstate` event dispatched in some browsers but first
          // history route not updated since async guard at the same time.
          // 不理解这种情况
          const location = getLocation(this.base);
          if (this.current === START && location === initLocation) {
            return;
          }
          // 更改路由视图
          this.transitionTo(location, route => {
            if (supportsScroll) {
              handleScroll(router, route, current, true);
            }
          });
        });
      }
    
      go(n: number) {
        window.history.go(n);
      }
    
      push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const { current: fromRoute } = this;
        // 这里需要手动调用push 方法 为什么不直接调用pushState方法?
        this.transitionTo(
          location,
          route => {
            pushState(cleanPath(this.base + route.fullPath));
            handleScroll(this.router, route, fromRoute, false);
            onComplete && onComplete(route);
          },
          onAbort
        );
      }
    
      replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const { current: fromRoute } = this;
        this.transitionTo(
          location,
          route => {
            replaceState(cleanPath(this.base + route.fullPath));
            handleScroll(this.router, route, fromRoute, false);
            onComplete && onComplete(route);
          },
          onAbort
        );
      }
    
      ensureURL(push?: boolean) {
        if (getLocation(this.base) !== this.current.fullPath) {
          const current = cleanPath(this.base + this.current.fullPath);
          push ? pushState(current) : replaceState(current);
        }
      }
    
      getCurrentLocation(): string {
        return getLocation(this.base);
      }
    }
    
    export function getLocation(base: string): string {
      let path = window.location.pathname;
      if (base && path.indexOf(base) === 0) {
        path = path.slice(base.length);
      }
      return (path || "/") + window.location.search + window.location.hash;
    }
    

    相关文章

      网友评论

          本文标题:四、History

          本文链接:https://www.haomeiwen.com/subject/njzqxhtx.html