Front-end routing jumping fundamentals

The current front-end trio of Angular, react, and vue all recommend a single-page application SPA development model that replaces the least-modified part of the DOM Tree in the routing switch to reduce the huge performance loss caused by the original multi-page application’s page jumps. They all have their own typical routing solutions, @angular/router, react-router, vue-router.

In general, these routing plugins always provide two different ways of routing: Hash and History, and sometimes also provide routing in non-browser environments Abstract, in vue-router is using the appearance pattern to provide a consistent high-level interface to several different routing methods, allowing us to switch between different routing methods in a more decoupled way.

It is worth mentioning that in addition to the difference in appearance between Hash and History, there is also a difference in that the Hash approach requires a separate pass for state preservation, while html5 History natively provides the ability to pass custom state, which we can use to pass information directly.

Let’s take a look at the features of both approaches and provide simple implementations for more complex features such as lazy loading, dynamic path matching, nested routes, route aliases, and more.

1. Hash

1.1 Related Api

Hash method is a # in the route, the main principle is the browser hashchange event triggered by listening to the URL path identifier change after the #, then get the current path identifier by getting location.hash, and then perform some route jumping operations, see MDN.

  1. location.href: returns the full URL
  2. location.hash: returns the anchor part of the URL
  3. location.pathname: returns the URL pathname
  4. hashchange event: this event is triggered when location.hash changes

For example, if you visit a path http://sherlocked93.club/base/#/page1, then the above values are

# http://sherlocked93.club/base/#/page1
{
  "href": "http://sherlocked93.club/base/#/page1",
  "pathname": "/base/",
  "hash": "#/page1"
}

Note: The Hash method uses the equivalent of a page anchor, so it conflicts with the original way of positioning the page scrolling by anchor positioning, resulting in positioning to the wrong route path, so you need to use other methods, before writing the progress-catalog plugin encountered this situation.

1.2 Example

Here is a simple implementation, the principle is to record the target route and the corresponding callbacks, click the jump trigger hashchange when the current path and execute the corresponding callbacks.

class RouterClass {
  constructor() {
    this.routes = {}        // Record the cb corresponding to the path identifier
    this.currentUrl = ''    // The hash is recorded only to facilitate the execution of cb
    window.addEventListener('load', () => this.render())
    window.addEventListener('hashchange', () => this.render())
  }
  
  /* Initialization */
  static init() {
    window.Router = new RouterClass()
  }
  
  /* Registering routes and callbacks */
  route(path, cb) {
    this.routes[path] = cb || function() {}
  }
  
  /* Record the current hash and execute cb */
  render() {
    this.currentUrl = location.hash.slice(1) || '/'
    this.routes[this.currentUrl]()
  }
}

If you want to use a script to control the fallback of a Hash route, you can log the route as it is experienced. The implementation of the route fallback jump is to assign a value to location.hash. But this will trigger a retriggered hashchange event and a second render. So we need to add a flag to indicate whether the render method is entering because of a fallback or a user jump.

class RouterClass {
  constructor() {
    this.isBack = false
    this.routes = {}        // Record the cb corresponding to the path identifier
    this.currentUrl = ''    // The hash is recorded only to facilitate the execution of cb
    this.historyStack = []  // hash stack
    window.addEventListener('load', () => this.render())
    window.addEventListener('hashchange', () => this.render())
  }
  
  /* Initialization */
  static init() {
    window.Router = new RouterClass()
  }
  
  /* Record path corresponding to cb */
  route(path, cb) {
    this.routes[path] = cb || function() {}
  }
  
  /* Stack current hash, execute cb */
  render() {
    if (this.isBack) {      // If it is entered by backoff, set false and return
      this.isBack = false   // Other operations are already done in the backoff method
      return
    }
    this.currentUrl = location.hash.slice(1) || '/'
    this.historyStack.push(this.currentUrl)
    this.routes[this.currentUrl]()
  }
  
  /* Routing backwards */
  back() {
    this.isBack = true
    this.historyStack.pop()                   // Remove the current hash and fall back to the previous
    const { length } = this.historyStack
    if (!length) return
    let prev = this.historyStack[length - 1]  // Get the target hash to fall back to
    location.hash = `#${ prev }`
    this.currentUrl = prev
    this.routes[prev]()                       // Execute the corresponding cb
  }
}

2. html5 History Api

2.1 Related Api

HTML5 provides some Api for routing operations, you can refer to this MDN article on the use, here is a list of common Api and their role, the specific parameters and what not will not be introduced, MDN on all have.

  1. history.go(n): route jump, for example, n is 2 pages forward, n is -2 is 2 pages backward, n is 0 is refresh page
  2. history.back(): route back, equivalent to history.go(-1)
  3. history.forward(): route forward, equivalent to history.go(1)
  4. history.pushState(): add a routing history, error if cross-domain URL is set
  5. history.replaceState(): replace the current page in the routing history
  6. popstate event: popstate event will be triggered when the active history changes, also triggered when clicking the browser’s forward/back button or when calling the first three methods above, see MDN

2.2 Example

The previous example is modified to use history.pushState to enter the stack and record the cb where the route jump is needed, and listen to the popstate event to get the parameters passed to pushState and execute the corresponding cb when moving forward and backward, because it borrows the browser’s own Api, so the code looks much neater.

class RouterClass {
  constructor(path) {
    this.routes = {}        // Record the cb corresponding to the path identifier
    history.replaceState({ path }, null, path)  // Access Status
    this.routes[path] && this.routes[path]()
    window.addEventListener('popstate', e => {
      const path = e.state && e.state.path
      this.routes[path] && this.routes[path]()
    })
  }
  
  /* Initialization */
  static init() {
    window.Router = new RouterClass(location.pathname)
  }
  
  /* Registering routes and callbacks */
  route(path, cb) {
    this.routes[path] = cb || function() {}
  }
  
  /* Jump routes and trigger callbacks corresponding to the routes */
  go(path) {
    history.pushState({ path }, null, path)
    this.routes[path] && this.routes[path]()
  }
}

Hash mode uses the URL’s Hash to simulate a full URL, so the page doesn’t reload when the URL changes. history mode changes the URL directly, so some address information is lost when the route jumps, and the static resource doesn’t match when refreshing or directly accessing the route address. So you need to configure some information on the server to add a candidate resource that covers all cases, such as jumping to index.html or something, which is generally the page your app depends on, and in fact libraries like vue-router also recommend this and provide common server configurations.

Leave a Reply