I used Vue to develop a single page application and found that no matter how the route changes, the browser address bar always has a ‘#’ number.

At that time, I checked my code and did not find the requested address with ‘#’. I was also puzzled at that time, but I did not care about it at that time because it did not affect the rendering of the page and send requests to the background. A recent look at vue-Router implementation revealed the mystery.

Two modes of vue-router (in browser environment)

1. Hash (corresponding to HashHistory)

The hash (” # “) symbol is supposed to be added to a URL to indicate the location of the page:

http://www.example.com/index.html#print
Copy the code

The # symbol itself and the character following it are called hash(that’s why I had a ‘#’ in the address bar before) and can be read using the window.location.hash property. It has the following characteristics:

  1. The hash appears in the URL but is not included in the HTTP request. It is used to direct browser action and is completely useless on the server side, so changing the hash does not reload the page

2. You can add listening events for hash changes:

window.addEventListener("hashchange", funcRef, false)
Copy the code
  1. Each change to hash (window.location.hash) adds a record to the browser’s access history

Using the above features of hash, it is possible to implement a front-end route that “updates the view without rerequesting the page.”

2. History (HTML5History)

The History interface is provided by the browser History stack. Through methods like back(), forward(), and Go (), we can read the browser History stack information and perform various jump operations.

Starting with HTML5, the History Interface provides two new methods: pushState() and replaceState() that allow us to modify the browser History stack:

window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)
Copy the code

StateObject: When the browser jumps to a new state, the popState event fires, which carries a copy of the stateObject parameter title: The title URL of the record added: the URL of the record added

The two methods have a common feature: when they are called to modify the browser history stack, the browser does not refresh the page even though the current URL has changed, which provides the basis for single-page application front-end routing “update the view without rerequesting the page.” Browser history can be thought of as a “stack.” A stack is a last-in, first-out structure, which can be thought of as a stack of plates. Each time a user clicks on a new web page, a new plate is added to it, called “pushing”. Each time the user clicks the Back button, the top plate is removed, called “out of the stack.” Each time the browser displays the contents of the top plate.

Vue – the function of the router

Vue-router is used to update the page view without re-requesting the page by changing the URL. Simply put, although the address bar has been changed, it is not a new page, but some parts of the previous page have been changed.

export default new Router({
  // mode: 'history'// routes: constantRouterMap})Copy the code

This is a common code for initializing vue-Router in Vue projects. Before I studied vue-Router carefully, I did not know that there is a mode attribute. Later I read related articles and learned that the mode attribute is used to specify which mode the vue-Router uses. If no mode value is specified, hash mode is used.

Source code analysis

First, take a look at the vue-Router constructor

constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = []  this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this)let mode = options.mode || 'hash'
    this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    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) : // AbstractHistory = new AbstractHistory(this, options.base)break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false, `invalid mode: ${mode}`)}}}Copy the code

Obtain the mode value first. If the mode value is history but the browser does not support the history mode, force the mode value to hash. History if supported. Next, based on the value of mode, choose which mode vue-Router will use.

case 'history':
        this.history = new HTML5History(this, options.base)
        break
case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
Copy the code

So there are two patterns. Once you’ve determined which mode vue-Router uses, it’s time to init. Let’s take a look at what the Router init method does, in SRC /index.js

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

    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (historyinstanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // .... // The VueRouter class exposes the following methods that are actually call-specifichistoryObject method push (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.history.push(location, onComplete, onAbort) } replace (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.history.replace(location, onComplete, onAbort) } }Copy the code

If it is HTML5History, execute

history.transitionTo(history.getCurrentLocation())
Copy the code

If the Hash mode is used, the command is executed

const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
Copy the code

As you can see, both modes execute the transitionTo() function. Let’s take a look at how the two modes are executed. First, let’s look at the Hash mode. Okay

HashHistory.push()

Let’s look at the push() method in HashHistory:

push (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.transitionTo(location, route => { pushHash(route.fullPath) onComplete && onComplete(route) }, onAbort) }function pushHash (path) {
  window.location.hash = path
}
Copy the code

The transitionTo() method is defined in the parent class to handle the underlying logic of route changes, while the push() method performs a direct hash assignment to the window:

Changes to window.location.hash = route.fullPath hash are automatically added to the browser’s access history.

To update a view, look at the transitionTo() method in the parent class History:

transitionTo (location: RawLocation, onComplete? : Function, onAbort? : Const route = this.router. Match (location,) {Function) {const route = this.router. this.current) this.confirmTransition(route, () => { this.updateRoute(route) ... }) } updateRoute (route: Route) { this.cb && this.cb(route) } listen (cb: Function) { this.cb = cb }Copy the code

As you can see, when the route changes, the this.cb method in History is called, and this. Cb is set by history.listen (cb). Going back to the VueRouter class definition, I found it set in the init() method:

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

  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}
Copy the code

The app in the code refers to the instance of Vue. The._route is not a built-in attribute defined in the component itself, but is globally registered as a hybrid with vue.mixin () when vue.use (Router) loads vue-Router. Affecting every Vue instance created after registration, the blend defines a reactive _route in the beforeCreate hook via vue.util.definereActive (). When the _route value changes, the render() method of the Vue instance is automatically called to update the view. Vm. render() is used to render the component corresponding to the route according to the path, name and other attributes of the current _route. So in summary, the update flow from route change to view is as follows:

this.$router.push(path) --> HashHistory.push() --> History.transitionTo() --> const route = this.router.match(location, This.current) will do the address matching, UpdateRoute (route) --> app._route=route (Vue instance _route changed) --> app._route=route (Vue instance _route changed) When the value of _route changes, Render () --> vm.render() in <router-view></router-view> render --> window.location.hash = route.fullpath (The browser address bar shows the path of the new route)Copy the code

HashHistory.replace()

Having said hashhistory.push (), it’s time to say hashhistory.replace ().

replace (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.transitionTo(location, route => { replaceHash(route.fullPath) onComplete && onComplete(route) }, onAbort) }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

As you can see, hashhistory.replace is structurally similar to push(), except that instead of assigning a value directly to window.location.hash, it calls the window.location.replace method to replace the route. This does not add the new route to the top of the browser’s access history stack, but replaces the current route.

Listening address bar

As you can see, the above procedures are all done inside the code, such as this.$router.push(), which is common in projects. Then set the browser’s address bar to the new hash value. What if you changed the route by typing the URL directly into the address bar, for example

setupListeners () {
  window.addEventListener('hashchange', () = > {if(! ensureSlash()) {return
    }
    this.transitionTo(getHash(), route => {
      replaceHash(route.fullPath)
    })
  })
}
Copy the code

This method setting listens for the browser event hashchange and calls the replaceHash function, meaning that entering the route directly into the browser address bar is equivalent to code calling the replace() method. The next steps are of course the same as hashHistory.replace () for page rendering.

HTML5History

The vue-Router code structure of HTML5History mode and the logic of updating the view are basically similar to hash mode and the steps of HashHistory mode. Only HashHistory push and replace () into HTML5History. PushState () and HTML5History replaceState ()

Adding a listener to HTML5History to modify the browser’s address bar URL is done directly in the constructor, listening for the popState event in HTML5History:

constructor (router: Router, base: ? string) { window.addEventListener('popstate', e => {
    const current = this.current
    this.transitionTo(getLocation(this.base), route => {
      if (expectScroll) {
        handleScroll(router, route, current, true)}})})}Copy the code

This is the analysis of the processing logic in the different modes of vue-Router hash mode and History mode.

Compare the two modes

The hash mode adds a ‘#’ to the browser URL. HTM5History does not have a ‘#’. The URL is the same as a normal URL. History.pushstate () has the following advantages over directly modifying hash:

  1. PushState sets the new URL to any URL of the same origin as the current URL. Hash can only change the part after #, so you can only set the URL of the current document
  2. PushState sets the new URL to be exactly the same as the current URL, which also adds the record to the stack. The new hash value must be different to trigger the record to be added to the stack
  3. PushState stateObject allows you to add any type of data to a record. Hash can only add short strings
  4. PushState sets the title property in addition for later use

Vue-router source code analysis – the overall process using the History API without refreshing change address bar