The principle and difference between vue-router’s hash and history

Single-page application (SPA) is becoming more and more mainstream front-end, a major feature of single-page application is the use of front-end routing, the front-end to directly control the route jump logic, and no longer controlled by the back-end staff, which gives the front-end more freedom.

Currently there are two main ways to implement front-end routing: hash mode and history mode, which are explained in detail below.

Hash mode

For example, when making an anchor jump with a hyperlink, you will find that the url is followed by “#id” and the hash value is the part of the url starting from the “#” sign to the end.

If we get the current hash value in the hashChange event, and modify the page content according to the hash value, we can achieve the purpose of front-end routing.

<!-- The menu href is set to the form of hash, and the id is the content of the page in which the app is placed -->
<ul id="menu">
    <li>
        <a href="#index">Index</a>
    </li>
    <li>
        <a href="#news">news</a>
    </li>
    <li>
        <a href="#user">user</a>
    </li>
</ul>

<div id="app"></div>
// Get the hash value in window.onhashchange and modify different content in the app according to the different values, which has the effect of routing
function hashChange(e){
    // console.log(location.hash)
    // console.log(location.href)
    // console.log(e.newURL)
    // console.log(e.oldURL)

    let app = document.getElementById('app')
    switch (location.hash) {
        case '#index':
            app.innerHTML = '<h1>This is the home page content</h1>'
            break
        case '#news':
            app.innerHTML = '<h1>This is the news content</h1>'
            break
        case '#user':
            app.innerHTML = '<h1>This is the personal center content</h1>'
            break
        default:
            app.innerHTML = '<h1>404</h1>'
    }
}
window.onhashchange = hashChange
hashChange()

The above implementation is rather rudimentary and we can encapsulate it a bit more.

class Router {
    constructor(){
        this.routers = []  // Store our routing configuration
    }
    add(route,callback){
        this.routers.push({
            path:route,
            render:callback
        })
    }
    listen(callback){
        window.onhashchange = this.hashChange(callback)
        this.hashChange(callback)()  // The hashchange is not triggered when you first enter the page, and must be called separately
    }
    hashChange(callback){
        let self = this
        return function () {
            let hash = location.hash
            console.log(hash)
            for(let i=0;i<self.routers.length;i++){
                let route = self.routers[i]
                if(hash===route.path){
                    callback(route.render())
                    return
                }
            }
        }
    }
}

let router = new Router()
router.add('#index',()=>{
    return '<h1>This is the home page content</h1>'
}) 
router.add('#news',()=>{
    return  '<h1>This is the news content</h1>'
})
router.add('#user',()=>{
    return  '<h1>This is the personal center content</h1>'
})
router.listen((renderHtml)=>{
    let app = document.getElementById('app')
    app.innerHTML = renderHtml
})

Implement a Router class, add a route configuration through the add method, the first parameter is the route path, the second parameter is the render function, which returns the html to be inserted into the page; through the listen method, listen to the hash changes and insert the html returned by each route, into the app. In this way we have implemented a simple hash route.

History mode

hash mode looks ugly, all with a “#” sign, we can also take history mode, history is the normal form of connection we usually see. history mode is based on the methods of window.history object.

In HTML4, the window.history object is already supported to control the page history jump, and the common methods include.

  • history.forward(): go one step forward in the history
  • history.back(): go back one step in the history
  • history.go(n): skip n steps in history, n=0 for refresh this page, n=-1 for backward page.

In HTML5, the window.history object has been extended with new APIs, including

  • history.pushState(data[,title][,url]): append a record to the history
  • history.replaceState(data[,title][,url]): replace the information of the current page in the history.
  • history.state: is a property to get the state information of the current page.
  • window.onpopstate: is an event that is triggered when the browser back button is clicked or js calls forward(), back(), go(). The listener function can be passed an event object, event.state is the data parameter passed in through the pushState() or replaceState() method

The principle of history pattern can be understood as follows, first we have to transform our hyperlinks, add onclick method to each hyperlink, block the default hyperlink jump, and instead use history.pushState or history.replaceState to change the url in the browser and modify the page content. Since the api adjustment through history does not initiate a request to the back end, it also serves the purpose of front-end routing.

If the user uses the browser’s forward-back button, it triggers the window.onpopstate event, which listens to the page to modify the page content according to the routing address.

It doesn’t have to be a hyperlink, any element can be used as a menu, as long as it is adjusted in the click event via history.

<!--html:-->
<ul id="menu">
    <li>
        <a href="/index">index</a>
    </li>
    <li>
        <a href="/news">news</a>
    </li>
    <li>
        <a href="/user">user</a>
    </li>

</ul>
<div id="app"></div>

// Change the hyperlink to block the default jump, which refreshes the page
document.querySelector('#menu').addEventListener('click',function (e) {
    if(e.target.nodeName ==='A'){
        e.preventDefault()
        let path = e.target.getAttribute('href')  // Get the href of the hyperlink and change it to pushState to jump without refreshing the page
        window.history.pushState({},'',path)  // Modify the url address displayed in the browser
        render(path)  // Change page content according to path
    }
})

function render(path) {
    let app = document.getElementById('app')
    switch (path) {
        case '/index':
            app.innerHTML = '<h1>This is the home page content</h1>'
            break
        case '/news':
            app.innerHTML = '<h1>This is the news content</h1>'
            break
        case '/user':
            app.innerHTML = '<h1>This is the personal center content</h1>'
            break
        default:
            app.innerHTML = '<h1>404</h1>'
    }
}
// Listens for browser forward and back events and renders the page based on the current path
window.onpopstate = function (e) {
    render(location.pathname)
}
// First time to enter the page to display the home page
render('/index')

This is too low, we can wrap it in a class, add a route through add method, jump through pushState, and change the jumping method of the hyperlink when initializing.

class Router {
    constructor(){
        this.routers = []
        this.renderCallback = null
    }
    add(route,callback){
        this.routers.push({
            path:route,
            render:callback
        })
    }
    pushState(path,data={}){
        window.history.pushState(data,'',path)
        this.renderHtml(path)
    }
    listen(callback){
        this.renderCallback = callback
        this.changeA()
        window.onpopstate = ()=>this.renderHtml(this.getCurrentPath())
        this.renderHtml(this.getCurrentPath())
    }
    changeA(){
        document.addEventListener('click', (e)=> {
            if(e.target.nodeName==='A'){
                e.preventDefault()
                let path = e.target.getAttribute('href')
                this.pushState(path)
            }
        })
    }
    getCurrentPath(){
        return location.pathname
    }
    renderHtml(path){
        for(let i=0;i<this.routers.length;i++){
            let route = this.routers[i]
            if(path===route.path){
                this.renderCallback(route.render())
                return
            }
        }
    }
}

let router = new Router()
router.add('/index',()=>{
    return '<h1>This is the home page content</h1>'
})
router.add('/news',()=>{
    return  '<h1>This is the news content</h1>'
})
router.add('/user',()=>{
    return  '<h1>This is the personal center content</h1>'
})
router.listen((renderHtml)=>{
    let app = document.getElementById('app')
    app.innerHTML = renderHtml
})

Of course, the above implementation is only a very elementary demo, and can not be used for real development scenarios, but only to deepen the understanding of the front-end routing.

The difference between Hash mode and History mode

  • hash mode is ugly, history mode is elegant
  • The new URL set by pushState can be any URL with the same source as the current URL; while hash can only modify the part after #, so only URLs with the same document as the current one can be set.
  • The new URL set by pushState can be exactly the same as the current URL, which will also add the record to the stack; while the new value set by hash must be different from the original to trigger the record to be added to the stack
  • pushState can add any type of data to the record via stateObject; hash can only add short strings
  • pushState can additionally set the title property for subsequent use
  • hash is compatible with IE8 or above, history is compatible with IE10 or above
  • history mode requires back-end cooperation to point all visits to index.html, otherwise the user will refresh the page, resulting in 404 errors.

Leave a Reply