跪拜 Guibai
← Back to the summary

Front-End Routing in 20 Lines: How Hash Routing Works

Background

Open a web page, the URL in the browser's address bar changes, and the page content updates accordingly — this is taken for granted in today's web applications. But before single-page applications (SPAs) became mainstream, a URL change almost always meant a full page refresh.

This article starts from the structure of a browser URL, dissects the problems of traditional multi-page navigation, then gradually explains how Hash routing finds a balance between "no page refresh" and "one-to-one mapping between URLs and resources," and finally provides a runnable HashRouter implementation.

How Traditional Multi-Page Navigation Works

Let's look at the simplest multi-page site. Two HTML files link to each other via <a> tags:

<!-- demo/index.html -->
<nav>
  <ul>
    <li><a href="index.html">Home</a></li>
    <li><a href="about.html">About Us</a></li>
  </ul>
</nav>
<main><h1>Home</h1></main>
<!-- demo/about.html -->
<nav>
  <ul>
    <li><a href="index.html">Home</a></li>
    <li><a href="about.html">About Us</a></li>
  </ul>
</nav>
<main><h1>About Us</h1></main>

When a user clicks a link, the browser performs the following steps:

  1. Makes an HTTP request to the server based on the URL in href
  2. The server returns a text/html response for the corresponding resource
  3. The browser receives the response, destroys the current page, and re-renders the entire document
  4. Inserts a new entry into the browser history

This process is inherently reasonable — URLs and resources have a one-to-one relationship, with each URL pointing to a complete HTML document. But in a mobile network environment, every navigation goes through "request → wait → white screen → re-render," and the experience noticeably suffers from page flickering.

The Demands of Single-Page Applications

The mobile era brought a change: page transitions within apps are instant, with no browser-level full-page refresh. User expectations for the web rose accordingly — content switching should be faster, and common areas (navigation bars, sidebars) should not be repeatedly destroyed and rebuilt.

The idea behind Single Page Applications (SPAs) is:

This raises a question: Should the URL still change?

If the URL doesn't change, users can't bookmark specific pages, nor can they use the browser's forward/back buttons. SPAs need a mechanism that can change the URL without triggering a full page refresh.

The Hash Part of a URL

A complete URL can be broken down into the following structure:

http(s)://www.example.com/path/to/page?q=search#/section
└──┬──┘  └─────┬──────┘ └─────┬─────┘ └──┬──┘ └───┬───┘
 protocol     host          path      query   hash/fragment

Hash is the part of the URL after #. Its original purpose was anchor links — marking a position within a long page, so that clicking scrolls the browser directly to that area without triggering a page refresh:

<a href="#bottom">Go to bottom</a>
<div style="height: 200vh;"></div>
<a name="bottom"></a>

Hash has two key characteristics:

  1. Modifying the hash does not cause the browser to make a request to the server. The content after # is not sent to the server.
  2. Modifying the hash updates the browser's address bar and inserts a new entry into the browser history.

These two characteristics exactly meet the needs of SPA front-end routing — the URL changes, the page doesn't refresh, and history entries are created.

The Design of Hash Routing

The core problem front-end routing needs to solve is: mapping different URLs (more precisely, different hashes) to different content rendering logic.

Breaking it down, three things are needed:

  1. Routing table: a data structure storing the mapping of "hash path → render function"
  2. Listening mechanism: the ability to detect and respond when the hash changes
  3. Rendering entry point: a fixed DOM mount point for replacing content

The browser provides the hashchange event to support the second point. Whenever the hash part of the URL changes (whether the user clicks an anchor link, manually edits the address bar, or JavaScript modifies location.hash), the hashchange event fires:

window.addEventListener('hashchange', function(event) {
  console.log('hash changed');
  console.log(event.newURL);  // the full URL after the change
  console.log(event.oldURL);  // the full URL before the change
});

Based on this event, a simple HashRouter class can be built.

Implementation

The following implementation comes from demo2/index.html:

class HashRouter {
  constructor() {
    // Routing table: stores the mapping of hash paths to callback functions
    this.routers = {};

    // Listen for the hashchange event
    // Note: 'this' in the event callback defaults to window
    // Use bind to bind 'this' to the HashRouter instance
    window.addEventListener('hashchange', this.load.bind(this));
  }

  // Register a route: associate a hash path with a callback function
  register(hash, callback) {
    this.routers[hash] = callback;
  }

  // Load a route: execute the corresponding callback based on the current hash
  load() {
    let hash = location.hash.slice(1);  // remove the leading #
    let handler;

    if (!hash) {
      // Do nothing when hash is empty (a default route could be set here)
    } else {
      handler = this.routers[hash];
    }

    handler.call(this);  // execute the callback, manually specifying 'this'
  }
}

Usage:

let router = new HashRouter();
let container = document.getElementById('container');

// Register three routes, each corresponding to different content rendering
router.register('/page1', () => (container.innerHTML = 'Page One'));
router.register('/page2', () => (container.innerHTML = 'Page Two'));
router.register('/page3', () => (container.innerHTML = 'Page Three'));

Navigation links in the page use hash-form hrefs:

<nav>
  <ul>
    <li><a href="#/page1">Page One</a></li>
    <li><a href="#/page2">Page Two</a></li>
    <li><a href="#/page3">Page Three</a></li>
  </ul>
</nav>
<div id="container"></div>

A few implementation details worth noting:

Looking Back: From Anchor Links to Front-End Routing

The readme.md summarizes this thread:

This is a classic pattern of "reusing existing infrastructure to solve a new problem." Browsers weren't specifically designed with a routing API for SPAs, but the combination of hash + hashchange happens to piece together all the behavior front-end routing needs.

Limitations of Hash Routing and Future Directions

Hash routing solves the most pressing problem of SPAs, but it also has clear limitations:

HTML5 introduced the History API (pushState / replaceState + popstate event), which can modify the full URL path without refreshing the page, eliminating the need for #. This is the next evolutionary direction after Hash routing, and its principle is in the same vein as the Hash routing discussed in this article — both establish a mapping between "no page refresh" and "mutable URLs."

Summary

What front-end routing needs to do can be summed up in one sentence: When the URL changes, use a piece of JavaScript logic to replace the browser's default navigation behavior, completing content replacement without a page refresh.

Hash routing is the most direct implementation of this idea: use the fragment after # as the path identifier, use the hashchange event as the trigger, and use a key-value pair as the routing table. The implementation code is barely twenty lines, but it draws a clear dividing line between traditional multi-page and modern SPAs.