This article looks at vue-Router source code from shallow to deep how to realize front-end routing through hash and History interface two ways, introduces the relevant principles, and compares the advantages and disadvantages of the two ways and matters for attention. Finally, it analyzes how to implement a Vue single page application that can be loaded directly from the file system without the help of the backend server.

With the business functions of front-end applications becoming more and more complex and users’ requirements for user experience becoming higher and higher, single page application (SPA) has become the mainstream form of front-end applications. One of the most notable features of large single-page applications is the use of a front-end routing system that updates the page view by changing the URL without re-requesting the page.

“Updating the view without rerequesting the page” is one of the core principles of front-end routing. At present, there are two main ways to realize this function in the browser environment:

  • Use the hash (” # “) in the URL
  • Take advantage of the History Interface new method in HTML5

Vue-router is a routing plug-in of vue. js framework. Below, we start from its source code and look at the code while looking at the principle. From shallow to deep, we observe how vue-Router realizes front-end routing through these two ways.

Model parameters

In vue-Router, the mode parameter controls the implementation mode of the route:

const router = new VueRouter({
  mode: 'history'.routes: [...]. })Copy the code

When an instance object of VueRouter is created, mode is passed in as a constructor parameter. Reading the source code with questions in mind, we can start with the definition of the VueRouter class. VueRouter class = VueRouter class = VueRouter class = VueRouter class = VueRouter class

export default class VueRouter {
  
  mode: string; // The string argument passed to indicate the history category
  history: HashHistory | HTML5History | AbstractHistory; // The actual object property in effect must be an enumeration of the above three classes
  fallback: boolean; // If the browser does not support this, the 'history' mode needs to be rolled back to the 'hash' mode
  
  constructor (options: RouterOptions = {}) {
    
    let mode = options.mode || 'hash' // The default is 'hash' mode
    this.fallback = mode === 'history' && !supportsPushState SupportsPushState check whether the browser supports the 'history' mode
    if (this.fallback) {
      mode = 'hash'
    }
    if(! inBrowser) { mode ='abstract' // Enforce 'abstract' mode when not running in a browser environment
    }
    this.mode = mode

    // Determine and instantiate the actual class of history according to 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)
        break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false.`invalid mode: ${mode}`)
        }
    }
  }

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

    // Perform initialization and listening operations according to the category of 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
      })
    })
  }

  // The VueRouter class exposes the following methods that actually call the concrete history objectpush (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

As can be seen:

  1. The string attribute mode, passed as an argument, is just a marker to indicate the implementation class of the object property history that is actually in play, as follows:

    mode history hash abstract
    history HTML5History HashHistory AbstractHistory
  2. If the browser does not support HTML5History (as determined by the supportsPushState variable), mode is forced to be set to ‘hash’. If not running in a browser environment, mode is forced to ‘abstract’

  3. VueRouter’s onReady(), push() and other methods are proxies that call the corresponding methods of the specific history object and perform different operations according to the specific class of the history object when initialized in init()

In the browser environment of the two ways, respectively is in HTML5History, HashHistory two classes to achieve. They are all defined in the SRC /history folder, inherits from the history class defined in the base.js file in the same directory. History is a definition of common and basic methods, so it’s confusing to look at them directly. Let’s start by looking at the friendly push() and replace() methods in HTML5History and HashHistory.

HashHistory

Review the principles before looking at the source code:

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

www.example.com/index.html#…

The # symbol itself and the character following it are called hashes and can be read using the window.location.hash property. It has the following characteristics:

  • 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

  • You can add listening events for hash changes:

    window.addEventListener("hashchange", funcRef, false)
    Copy the code
  • 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.”

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:

window.location.hash = route.fullPath
Copy the code

Hash changes 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? :Function) {
  const route = this.router.match(location, 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

According to the notes, APP is an instance of Vue component, but we know that as a progressive front-end framework, Vue should not have the built-in attribute _route in its component definition. If there is such attribute in the component, it should be in the place where the plug-in is loaded. VueRouter’s install() method is mixed into the Vue object.

export function install (Vue) {
  
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      }
      registerInstance(this.this)}})}Copy the code

The vue.mixin () method globally registers a mix that affects every Vue instance created after registration. The mix defines the reactive _route attribute 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.

To summarize, the flow from setting up route changes to view updates is as follows:

$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
Copy the code

HashHistory.replace()

The replace() method differs from the push() method in that instead of adding a new route to the top of the browser’s access history stack, it replaces the current route:

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('#') window.location.replace( window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path ) }Copy the code

As you can see, it is basically similar to push() in implementation structure, except that instead of assigning window.location.hash directly, it calls window.location.replace to replace the route.

Listening address bar

Vuerouter.push () and vuerouter.replace () discussed above can be called directly from the logical code of the Vue component. In addition, in the browser, the user can enter the change route directly into the browser address bar. So VueRouter also needs to be able to listen for routing changes in the browser address bar and have the same response behavior as calling from code. In HashHistory this is done via setupListeners:

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

HTML5History

History Interface is the interface provided by the browser History stack. By using methods such as 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: Title of the record to be added
  • URL: INDICATES the URL of the record to be added

These two methods have a common feature: When they are called to modify the browser history stack, the browser won’t attempt to load this URL after a call to pushState() This provides the basis for single-page application front-end routing “update the view without rerequesting the page.”

Vue router router router router router router router

push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
  const { current: fromRoute } = this
  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)
}

// 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) {
      history.replaceState({ key: _key }, ' ', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, ' ', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

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

The structure of the code and the logic of updating the view is basically similar to the hash mode, Just assign window.location.replace() directly to window.location.hash instead of calling the history.pushState() and history.replacestate () methods.

Adding a listener to HTML5History to modify the browser’s address bar URL is done directly in the constructor:

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

HTML5History, of course, uses HTML5’s new feature, which requires browser version-specific support. This is checked using the supportsPushState variable:

// src/util/push-state.js
export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf('Android 2.')! = = -1 || ua.indexOf('the Android 4.0')! = = -1) &&
    ua.indexOf('Mobile Safari')! = = -1 &&
    ua.indexOf('Chrome') = = = -1 &&
    ua.indexOf('Windows Phone') = = = -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})()
Copy the code

This is the introduction to the source code of hash mode and History mode, both of which are implemented through the browser interface. In addition, Vue-Router has prepared an Abstract mode for non-browser environment, which uses an array stack to simulate the function of the browser history stack. Of course, the above is just some core logic, in order to ensure the robustness of the system, there are a lot of auxiliary logic in the source code, but also worth learning. In addition, in vue-Router there are routing matching, router-view view components and other important parts, about the overall source code reading recommended didi front-end this article

Compare the two modes

In general requirements scenarios, the Hash mode is similar to the history mode, but almost all articles recommend using the history mode for the reason that the “#” symbol is ugly… 0 _0. “”

If we don’t want ugly hashes, we can use the route’s history mode — official document

Of course, we should definitely not use the level of appearance to evaluate the quality of technology. According to MDN, calling history.pushState() has the following major advantages over modifying hash directly:

  • 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
  • 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
  • PushState stateObject allows you to add any type of data to a record. Hash can only add short strings
  • PushState sets the title property in addition for later use

A problem with history mode

We know that for single-page applications, the ideal scenario is to load index.html only when entering the application, and the subsequent network operations are completed through Ajax, without re-requesting the page according to the URL. However, it is inevitable to encounter special situations, such as users directly enter the address bar and enter, and the browser restarts to reload the application.

Hash mode changes only the hash part of the content, which is not included in the HTTP request:

http://oursite.com/#/user/id // If a new request is made, only http://oursite.com/ will be sentCopy the code

Therefore, page requests based on urls in hash mode are not a problem.

The history mode changes the URL to the same as the URL on the normal request back end

http://oursite.com/user/id
Copy the code

In this case, the request is sent to the back end again. If the route processing for /user/ ID is not configured on the back end, a 404 error is displayed. The official recommended solution is to add a candidate resource on the server that covers all cases: if the URL doesn’t match any static resource, it should return the same index.html page that your app relies on. At the same time, the server no longer returns the 404 error page because the index.html file is returned for all paths. To avoid this, override all routing cases in the Vue application and then render a 404 page. Alternatively, if node.js is used as the background, you can use server-side routing to match urls and return 404 if no route is matched, thus implementing a Fallback.

Load application files directly

Tip: built files are meant to be served over an HTTP server.

Opening index.html over file:// won’t work.

After the Vue project is packaged with the VUe-CLI webpack, the command line will be prompted with this paragraph. Typically, whether in development or online, front-end projects are accessed through the server. There is no “Opening index.html over file://”, but as programmers know, requirements and scenarios are always strange, and only you think of them, not the product manager.

The original intention of writing this paper is to meet such a problem: In need of rapid development of a mobile display project, we decided to use the form of WebView loading Vue single-page application, but there is no back-end server to provide, so all resources need to be loaded from the local file system:

// AndroidAppWrapper public class MainActivity extends AppCompatActivity { private WebView webView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); webView = new WebView(this); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl("file:///android_asset/index.html"); setContentView(webView); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) { webView.goBack(); return true; } return false; }}Copy the code

In this case, I must “open index.html over file://”, so I need to do a few things first

  • Change the value of the assetsPublicPath field to the relative path ‘./’ in the project config.js file
  • Adjust the position of static resources such as images in the generated static folder to match the reference addresses in the code

This is an obvious change that needs to be made, but it still cannot be loaded smoothly after the change. After repeated investigation, it was found that the router was set to the history mode during the development of the project (for aesthetics… 0_0″), when changed to hash mode can be normally loaded.

Why does this happen? The reasons may be as follows:

When loading index.html directly from the file system, the URL is:

file:///android_asset/index.html
Copy the code

The home view must match the path: ‘/’ :

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'index',
      component: IndexView
    }
  ]
})
Copy the code

Let’s start with history mode. HTML5History:

ensureURL (push? : boolean) { if (getLocation(this.base) ! == this.current.fullPath) { const current = cleanPath(this.base + this.current.fullPath) push ? pushState(current) : replaceState(current) } } 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 logic only ensures that there is a URL, the path is clipped directly from window.location.pathname, and it ends in index.html, so it doesn’t match a ‘/’, Openindex. HTML over file:// won’t work

Look at the hash pattern again, in HashHistory:

export class HashHistory extends History { constructor (router: Router, base: ? string, fallback: boolean) { ... ensureSlash() } // this is delayed until the app mounts // to avoid the hashchange listener being fired too early setupListeners () { window.addEventListener('hashchange', () => { if (! ensureSlash()) { return } ... }) } getCurrentLocation () { return getHash() } } function ensureSlash (): boolean { const path = getHash() if (path.charAt(0) === '/') { return true } replaceHash('/' + path) return false } export function getHash (): string { const href = window.location.href const index = href.indexOf('#') return index === -1 ? '' : href.slice(index + 1) }Copy the code

EnsureSlash () returns true when # is followed by a ‘/’, otherwise the ‘/’ is forcibly inserted, so we can see that even if we open index.html from the file system, the URL will still have the following form:

file:///C:/Users/dist/index.html#/
Copy the code

The getHash() method returns a path of ‘/’ that matches the route in the home view.

In order to load a Vue single-page application directly from the file system without using a back-end server, you need to make sure that the Vue -router uses hash mode, in addition to some packaged path Settings.

Transport links