When we use front-end rendering like Vue or React, there are usually two types of routing: hash routing and history routing.

  1. hash routing: listen to the hash changes in the url and render different content, this kind of routing does not send requests to the server and does not need server-side support.
  2. history routing: listens for changes in the path in the url and requires the support of both the client and the server.

Let’s implement these two types of routes step by step to understand the underlying implementation principles in depth. We mainly implement the following simple functions.

  1. listen to changes in the route and can make actions when the route changes.
  2. the ability to move forward or backward.
  3. can configure routes.

1. hash routing

When the hash of a page changes, it will trigger the hashchange event, so we can listen to this event to determine if the route has changed.

1
2
3
4
5
6
7
8
9
window.addEventListener(
    'hashchange',
    function (event) {
        const oldURL = event.oldURL; // 上一个URL
        const newURL = event.newURL; // 当前的URL
        console.log(newURL, oldURL);
    },
    false
);

1.1 The process of implementation

After splitting oldURL and newURL, we can get more detailed hash values. We’ll start by creating a HashRouter class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class HashRouter {
    currentUrl = ''; // 当前的URL
    handlers = {};

    getHashPath(url) {
        const index = url.indexOf('#');
        if (index >= 0) {
            return url.slice(index + 1);
        }
        return '/';
    }
}

The event hashchange is only triggered when the hash changes, and it is not triggered when we first enter the page, so we also need to listen to the load event. Note that the event of the two events is different: the event in the hashchange event has two properties, oldURL and newURL, but the event in the load event does not have these two properties, but we can get the current hash route through location.hash.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class HashRouter {
    currentUrl = ''; // 当前的URL
    handlers = {};

    constructor() {
        this.refresh = this.refresh.bind(this);
        window.addEventListener('load', this.refresh, false);
        window.addEventListener('hashchange', this.refresh, false);
    }

    getHashPath(url) {
        const index = url.indexOf('#');
        if (index >= 0) {
            return url.slice(index + 1);
        }
        return '/';
    }

    refresh(event) {
        let curURL = '',
            oldURL = null;
        if (event.newURL) {
            oldURL = this.getHashPath(event.oldURL || '');
            curURL = this.getHashPath(event.newURL || '');
        } else {
            curURL = this.getHashPath(window.location.hash);
        }
        this.currentUrl = curURL;
    }
}

Up to this point it has been possible to get the current hash route, but when the route changes, our page should switch, so we need to listen for this change.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class HashRouter {
    currentUrl = ''; // 当前的URL
    handlers = {};

    // 暂时省略上面的代码

    refresh(event) {
        // 当hash路由发生变化时,则触发change事件
        this.emit('change', curURL, oldURL);
    }

    on(evName, listener) {
        this.handlers[evName] = listener;
    }
    emit(evName, ...args) {
        const handler = this.handlers[evName];
        if (handler) {
            handler(...args);
        }
    }
}
const router = new HashRouter();
rouer.on('change', (curUrl, lastUrl) => {
    console.log('当前的hash:', curUrl);
    console.log('上一个hash:', lastUrl);
});

1.2 The way to call

By now, we have the basic functionality done. Let’s match it with an example to get a better image.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 先定义几个路由
const routes = [
    {
        path: '/',
        name: 'home',
        component: <Home />,
    },
    {
        path: '/about',
        name: 'about',
        component: <About />,
    },
    {
        path: '*',
        name: '404',
        component: <NotFound404 />,
    },
];
const router = new HashRouter();
// 监听change事件
router.on('change', (currentUrl, lastUrl) => {
    let route = null;
    // 匹配路由
    for (let i = 0, len = routes.length; i < len; i++) {
        const item = routes[i];
        if (currentUrl === item.path) {
            route = item;
            break;
        }
    }
    // 若没有匹配到,则使用最后一个路由
    if (!route) {
        route = routes[routes.length - 1];
    }
    // 渲染当前的组件
    ReactDOM.render(route.component, document.getElementById('app'));
});

See [Sample of hash routing].

2. history routing

In history routing, we will definitely use the methods in window.history, the common actions are

  • back(): go back to the previous route.
  • forward(): advance to the next route, if any.
  • go(number): go to any route, positive number for forward, negative number for backward.
  • pushState(obj, title, url): advances to the specified URL without refreshing the page.
  • replaceState(obj, title, url): replaces the current route with the url, without refreshing the page.

When calling these methods, all will just modify the URL of the current page, and the content of the page will not change at all. If an interviewer asks the question “how to modify the URL of a page without sending a request”, the answer is these 5 methods. then the answer is these 5 methods.

If there is no new updated URL on the server side, a browser refresh will report an error, because a browser refresh will actually send an http web request to the server. So to use history routing, you need the support of the server.

2.1 Application scenarios

What is the difference between the pushState and replaceState methods and location.href and location.replace methods? What are the application scenarios?

  1. location.href and location.replace send a request to the server when switching, while pushState and replace only modify the url, unless a request is initiated.
  2. the feature that only switches the url without sending a request can be used in front-end rendering, for example, the home page is rendered server-side and the secondary page is rendered front-end.
  3. the possibility of adding animations for route switching.
  4. when using a scenario like Jitterbug in the browser, the corresponding URL can be silently modified when the user slides to switch videos, and when the user refreshes the page, it can still stay in the current video.

2.2 Unable to listen for route changes

When we use history routing, we must be able to listen for changes to the route. There is a global popstate event, which has a state keyword in its name, but when pushState and replaceState are called, the popstate event is not triggered, only the first 3 methods listed above are triggered. You can click popState does not trigger popstate event to see it.

In this case, we can use window.dispatchEvent to add an event.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const listener = function (type) {
    var orig = history[type];
    return function () {
        var rv = orig.apply(this, arguments);
        var e = new Event(type);
        e.arguments = arguments;
        window.dispatchEvent(e);
        return rv;
    };
};
window.history.pushState = listener('pushState');
window.history.replaceState = listener('replaceState');

Then you can add a listener for both methods.

1
2
window.addEventListener('pushState', this.refresh, false);
window.addEventListener('replaceState', this.refresh, false);

2.3 The complete code

The complete code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class HistoryRouter {
    currentUrl = '';
    handlers = {};

    constructor() {
        this.refresh = this.refresh.bind(this);
        this.addStateListener();
        window.addEventListener('load', this.refresh, false);
        window.addEventListener('popstate', this.refresh, false);
        window.addEventListener('pushState', this.refresh, false);
        window.addEventListener('replaceState', this.refresh, false);
    }
    addStateListener() {
        const listener = function (type) {
            var orig = history[type];
            return function () {
                var rv = orig.apply(this, arguments);
                var e = new Event(type);
                e.arguments = arguments;
                window.dispatchEvent(e);
                return rv;
            };
        };
        window.history.pushState = listener('pushState');
        window.history.replaceState = listener('replaceState');
    }
    refresh(event) {
        this.currentUrl = location.pathname;
        this.emit('change', location.pathname);
        document.querySelector('#app span').innerHTML = location.pathname;
    }
    on(evName, listener) {
        this.handlers[evName] = listener;
    }
    emit(evName, ...args) {
        const handler = this.handlers[evName];
        if (handler) {
            handler(...args);
        }
    }
}
const router = new HistoryRouter();
router.on('change', function (curUrl) {
    console.log(curUrl);
});

The usage is the same as hash routing above, so we won’t go into details here. Click to see the application of history routing.

3. Summary

At this point, the principle and implementation of the two routing methods are introduced.