vue-router源码分析(五)

274 阅读1分钟

五、hash模式url如何变化

hash模式下,调用this.$router.push或点击router-link标签,路由的hash会发生相应的变化,当点击浏览器的回退,会回到上一次的hash路径。那么他的跳转是调动了VueRouterclass中的实例方法push,也就是this.history.push(location, onComplete, onAbort),在mode hash下,history的push是hash.js文件中的HashHistoryHashHistory中的push,首先会拿到current,也就是跳转前的路由,然后会执行this.transitionTo,执行成功后导航守卫部分逻辑完成,页面完成渲染,之后首先会调用pushHash(route.fullPath)pushHash函数首先会判断当前的浏览器环境,设备版本等等,如果不支持window.history.pushState,那么会调用 window.location.hash = path,对hash进行切换。如果支持history.pushState,那么会执行pushState(getUrl(path)),getUrl方法,首先会通过window.location.href拿到当前的路径,然后通过indexOf('#')拿到#的index,目的是获取#之前的baseUrl,最后把传入的path和baseUrl拼接成一个完整的路径返回。也就是说调用pushState传入的是一个最终的完整路径。pushState会判断当前跳转的方式是否是replace,如果是replace那么会最终调用history.replaceState,否则也就是push方法,即history.pushState,这两者,replace会替换histroy的历史栈的当前记录,push则会往历史栈添加指定的url。a->b,当前在b,调用push(c),历史栈为 a->b->c 点击返回会到b,而使用replace,则会把历史栈修改为a->c,点击返回会回到a。如果在Safari浏览器中,某些情况下history堆栈长度会被限制在100,如果出现了问题,那么他会执行,window.location[replace ? 'replace' : 'assign'](url)进行跳转。

// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      // preserve existing history state as it could be overriden by the user
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

那么当点击浏览器的回退按钮,视图也会发生相应的改变,也就是说,vue-router帮助我们监听了这一事件。在根部Vue实例触发beforecreate的时候,会触发init函数,在触发history.transitionTo进行首次跳转之后,会触发回调函数setupListenerssetupListeners函数会执行history.setupListeners()函数,在hash模式下,会对应到HashHistorysetupListeners函数,这个函数首先会通过window.addEventListener添加一个监听事件,监听事件的类型会根据当前浏览器对history的支持度,监听popstatehashchange事件,当我们点击浏览器的回退按钮的时候,会触发回调函数handleRoutingEvent,handleRoutingEvent函数会通过getHash()拿到要跳转的hash值作为参数,调用transitionTo完成跳转

// src/history/hash.js
export class HashHistory extends History {
  ...
  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {
    ...
    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    ...
  }
}

当我们输入http://localhost:8080的时候,vue-router会帮助我们自动加上/#/。在classHashHistory初始化的时候会执行ensureSlash函数,ensureSlash函数首先会通过getHash方法拿到hash,如果我们输入的是没有#符号的url,那么会return一个空的字符串,之后会走replaceHash('/' + path)方法,replaceHash在调用replaceState window.location.replace,会把getUrl(path)作为参数,getUrl会把传入的path和base做一个拼接${base}#${path},所以会帮助我们拼接出一个带有#号的初始url,另外这部分用的是replaceStatewindow.location.replace这也是为了防止把我们输入的不带#号的url作为历史栈

// src/history/hash.js
export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
  ...
  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }
  ...
}
...
function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf('#')
  // empty path
  if (index < 0) return ''
  href = href.slice(index + 1)
  return href
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}