preface

Front-end routing modes include Hash mode and History mode.

When the vue-router is initialized, it uses different routing modes according to mode, and thus generates different object instances. For example, HTML5History is used for history mode and HashHistory is used for hash mode.

init (app: any /* Vue component instance */) {
  this.app = app

  const { mode, options, fallback } = this
  switch (mode) {
    case 'history':
      this.history = new HTML5History(this, options.base)
      break
    case 'hash':
      this.history = new HashHistory(this, options.base, fallback)
      break
    case 'abstract':
      this.history = new AbstractHistory(this)
      break
    default:
      assert(false.`invalid mode: ${mode}`)}this.history.listen(route= > {
    this.app._route = route
  })
}
Copy the code

This time we will focus on the implementation of HTML5History and HashHistory.

HashHistory

Vue-router implements Hash routing by creating a HashHistory.

this.history = new HashHistory(this, options.base, fallback)
Copy the code

The three parameters represent:

  • This: Router instance
  • Base: indicates the base path of the application
  • Fallback: History mode, but does not support History and is converted to Hash mode

HashHistory inherits from the History class, with some attributes and methods coming from the History class. Take a look at the HashHistory constructor.

constructor

Constructors do four main things.

  1. Call the superclass constructor through super, but let’s put that aside.
  2. Handles the case where History mode is converted to Hash mode because History is not supported.
  3. Make sure # is followed by a slash, if not.
  4. Implementation jumps to the hash page and listens for hash change events.
constructor(router: VueRouter, base: ? string, fallback: boolean) {super(router, base)

  // check history fallback deeplinking
  if (fallback && this.checkFallback()) {
    return
  }

  ensureSlash()
  this.transitionTo(getHash(), () => {
    window.addEventListener('hashchange', () = > {this.onHashChange()
    })
  })
}
Copy the code

Let’s talk about these in detail.

checkFallback

The second thing the constructor does first, when fallback is true, is that older browsers (IE9) do not support History mode, so it will be relegated to Hash mode.

The URL is also checked using the checkFallback method.

checkFallback () {
  // Remove the base prefix
  const location = getLocation(this.base)

  // If it does not start with /#
  if (!/ # ^ / / /.test(location)) {
    window.location.replace(
      cleanPath(this.base + '/ #' + location)
    )
    return true}}Copy the code

The getLocation method is used to remove the base prefix, followed by the re to determine if the URL begins with /#. If not, replace the URL with one that begins with /#. Constructor () since switching routes using Hash URLS under IE9 causes the entire page to refresh, the listener hashchange does not work, so return directly.

Take a look at the implementation of the getLocation and cleanPath methods called in checkFallback.

The getLocation method basically removes the base prefix. If you search for base in the vue-Router official document, you can find that it is the base path of the application.

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
}
Copy the code

The cleanPath method replaces the double slash with a single slash to ensure that the URL path is correct.

export function cleanPath (path: string) :string {
  return path.replace(/\/\//g.'/')}Copy the code

ensureSlash

Let’s look at the third thing the constructor does.

All the ensureSlash method does is make sure the URL root path has a slash, or if it doesn’t.

function ensureSlash () :boolean {
  const path = getHash()
  if (path.charAt(0) = = ='/') {
    return true
  }
  replaceHash('/' + path)
  return false
}
Copy the code

EnsureSlash uses getHash to retrieve the path after the # sign of the URL and replaceHash to replace the route.

function getHash () :string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  const href = window.location.href
  const index = href.indexOf(The '#')
  return index === - 1 ? ' ' : href.slice(index + 1)}Copy the code

Hash is not available via window.location.href due to Firefox (as stated in the source code comment), but window.location.href.

function replaceHash (path) {
  const i = window.location.href.indexOf(The '#')
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + The '#' + path
  )
}
Copy the code

What the replaceHash method does is replace the hash route after the # symbol.

onHashChange

Finally, look at the fourth thing the constructor does.

this.transitionTo(getHash(), () => {
  window.addEventListener('hashchange', () = > {this.onHashChange()
  })
})
Copy the code

TransitionTo is a method of the parent class History, which is more complex and mainly realizes the function of guard navigation. I’ll leave it here for now, and I’ll go into it later.

The next step is to listen for the Hashchange event and call the onHashChange method when the hash route changes.

onHashChange () {
  if(! ensureSlash()) {return
  }
  this.transitionTo(getHash(), route => {
    replaceHash(route.fullPath)
  })
}
Copy the code

When the hash route is changed, that is, the page is redirected, the route starts with a slash, the guard navigation is triggered, and a new hash route is replaced.

HashHistory also implements push, replace, go and other programmatic navigation. If you are interested, you can directly look at the source code. Here is not a description, mainly using the above method to achieve.

HTML5History

Vue-router implements History mode routing by new an HTML5History.

this.history = new HTML5History(this, options.base)
Copy the code

HTML5History is also an inheritance and History class.

constructor

The HTML5History constructor does a few things:

  1. Call the parent classtransitionToMethod to trigger guard navigation, more on that later.
  2. Listening to thepopstateEvents.
  3. Listens for scroll bar scrolling if there is scrolling behavior.
constructor(router: VueRouter, base: ? string) {super(router, base)

  this.transitionTo(getLocation(this.base))

  const expectScroll = router.options.scrollBehavior
  window.addEventListener('popstate', e => {
    _key = e.state && e.state.key
    const current = this.current
    this.transitionTo(getLocation(this.base), next => {
      if (expectScroll) {
        this.handleScroll(next, current, true)}})})if (expectScroll) {
    window.addEventListener('scroll', () => {
      saveScrollPosition(_key)
    })
  }
}
Copy the code

Let’s talk about these in detail.

scroll

Let’s start by listening for scrollbar events.

window.addEventListener('scroll', () => {
  saveScrollPosition(_key)
})
Copy the code

After the scroll bar is rolled, vue-Router saves the scroll bar location. There are two things to know here, the saveScrollPosition method and the _key.

const genKey = (a)= > String(Date.now())
let _key: string = genKey()
Copy the code

_key is a current timestamp that is passed as a parameter every time the browser moves forward or backward, so that the page to which it jumps can be retrieved. So what does _key do.

Take a look at the implementation of saveScrollPosition:

export function saveScrollPosition (key: string) {
  if(! key)return
  window.sessionStorage.setItem(key, JSON.stringify({
    x: window.pageXOffset,
    y: window.pageYOffset
  }))
}
Copy the code

Vue-router stores the scrollbar location in sessionStorage, where the key is _key.

So every time the browser scrolls, the location of the scroll bar will be saved in sessionStorage for later use.

popstate

The popState event is triggered when the browser moves forward or backward. Guard navigation is also triggered by the transitionTo call, or the handleScroll method if there is scrolling behavior.

The handleScroll method has a lot of code, so let’s take a look at how the scrolling behavior is used.

scrollBehavior (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0.y: 0}}}Copy the code

If you want to simulate the behavior of “rolling to anchor” :

scrollBehavior (to, from, savedPosition) {
  if (to.hash) {
    return {
      selector: to.hash
    }
  }
}
Copy the code

So there’s at least three things to determine, one is the save position, one is the selector, and one is the XY coordinate.

HandleScroll (delete some judgments) :

handleScroll (to: Route, from: Route, isPop: boolean) {
  const router = this.router
  const behavior = router.options.scrollBehavior

  // wait until re-render finishes before scrolling
  router.app.$nextTick((a)= > {
    let position = getScrollPosition(_key)
    const shouldScroll = behavior(to, from, isPop ? position : null)
    if(! shouldScroll) {return
    }
    const isObject = typeof shouldScroll === 'object'
    if (isObject && typeof shouldScroll.selector === 'string') {
      const el = document.querySelector(shouldScroll.selector)
      if (el) {
        position = getElementPosition(el)
      } else if (isValidPosition(shouldScroll)) {
        position = normalizePosition(shouldScroll)
      }
    } else if (isObject && isValidPosition(shouldScroll)) {
      position = normalizePosition(shouldScroll)
    }

    if (position) {
      window.scrollTo(position.x, position.y)
    }
  })
}
Copy the code

We start with an if judgment, and if there’s a selector, we get the coordinates of the corresponding element.

Otherwise, the value returned by scrollBehavior is used as the coordinate, which may be the coordinate of savedPosition or a custom XY coordinate.

After a series of checks, the window.scrollTo method is finally called to set the scrollbar position.

There are three methods used to process coordinates, respectively:

  • GetElementPosition: Gets element coordinates
  • IsValidPosition: Verifies that the coordinates are valid
  • NormalizePosition: Formats coordinates

The amount of code is not large, the specific code details interested can take a look.

HTML5History also implemented push, replace, go and other programmatic navigation.

The last

At this point, the implementation of HashHistory and HTML5History is generally understood. As we read, we kept coming across the parent class History and its transitionTo method, which we’ll learn more about in the next article.