跪拜 Guibai
← Back to the summary

How the Humble URL Hash Fragment Became the Foundation of Single-Page Apps

This article starts from a simple multi-page demo, derives the core idea of Single Page Applications (SPA) step by step, and implements a HashRouter with vanilla JavaScript to help you truly understand what front-end routing actually does.

1. The Traditional Multi-Page Era: Every Navigation is a "Rebirth"

Let's look at a very basic piece of HTML:

<!-- index.html Homepage -->
<header>
    <nav>
        <ul>
            <li><a href="http://127.0.0.1:5501/fe/history/demo/index.html">Home</a></li>
            <li><a href="http://127.0.0.1:fe/history/demo/about.html">About Us</a></li>
        </ul>
    </nav>
</header>
<main>
    <h1>Home</h1>
</main>

The structure of about.html is almost identical, only the text inside <h1> is different.

This is the traditional Multi-Page Application (MPA) development model. When a user clicks an <a> tag, the browser goes through this complete process:

  1. The browser takes the URL and initiates a request to the server via the HTTP protocol.
  2. The server processes the request and returns the corresponding text/html response.
  3. The browser receives the response data and re-renders the entire page.
  4. A new entry is inserted into the browsing history.

What's the problem? Every navigation causes the entire page to re-render from a blank screen. In the PC era, network speeds were tolerable, but in the mobile era—a flash of white, a flicker—the experience becomes unacceptable.

Even worse: often 90% of the structure (header, nav, footer) between two pages is exactly the same, with only the content inside <main> being different. Re-rendering the entire page is pure waste.

2. The Core Contradiction: The URL Must Change, but the Page Cannot Refresh

We want a mechanism that "allows the URL to change, but does not trigger a full page refresh."

Why must the URL change? Because the URL is the unique identifier for a resource and its state. Users need the URL to share a specific page, use the browser's forward/back buttons, and return to the correct position after a refresh. If the URL doesn't change, all these capabilities are lost.

So, is there a part of the URL that can change without triggering a request?

Yes—the hash.

3. Deconstructing the URL Structure

Let's review the structure of a complete URL:

http(s)://www.baidu.com/u/123?a=1&b=2#/page1
  ──┬──    ────┬───── ─┬─ ───┬─── ──┬───
  protocol    host    path  query  hash

Key point: Changes to the hash part do not trigger an HTTP request. The browser does not send #/page1 to the server.

The original design purpose of the hash was anchor links—marking a position within a long page for direct navigation:

<a name="top"></a>
<a href="#bottom">Go to bottom</a>
<div style="height:200vh;background-color:yellow;"></div>
<a href="#top">Go to top</a>
<div style="height:300vh;background-color:green;"></div>
<a name="bottom"></a>

We can verify what the browser does when the hash changes with a piece of code:

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

After running this, you'll find: when a link is clicked, the hash part of the URL changes, the hashchange event fires, but the page does not refresh, and no network request is made.

This is the cornerstone of front-end routing.

4. From Anchor to Router: The Birth of SPA

Since changing the hash doesn't refresh the page and can trigger an event, we can do this:

The page structure becomes:

<header>
    <nav>
        <ul>
            <li><a href="#/page1">Page 1</a></li>
            <li><a href="#/page2">Page 2</a></li>
            <li><a href="#/page3">Page 3</a></li>
        </ul>
    </nav>
</header>
<!-- Only this container needs dynamic modification -->
<div id="container"></div>

The entire page is just a single HTML file, and #container is the sole "mount point". All page switching is essentially partial DOM replacement.

This is the core idea of a Single Page Application (SPA).

5. Writing a HashRouter by Hand

Now that we understand the principle, let's implement a minimal front-end router using vanilla JavaScript:

class HashRouter {
    constructor() {
        // Routing table: hash -> callback function
        this.routers = {};
        // Listen for hash changes, trigger loading
        // Note: In event callbacks, 'this' defaults to window, so bind is needed
        window.addEventListener('hashchange', this.load.bind(this));
    }

    // Register a route
    register(hash, callback) {
        this.routers[hash] = callback;
    }

    // Execute the corresponding callback based on the current hash
    load() {
        console.log(this);
        // In a real project, you would parse location.hash and look up the routing table here
    }
}

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

router.register('/page1', () => container.innerHTML = 'Page One');
router.register('/page2', () => container.innerHTML = 'Page Two');
router.register('/page3', () => container.innerHTML = 'Page Three');

Although simple, this code already contains the three core elements of front-end routing:

Element Corresponding Code Role
Routing Table this.routers = {} The mapping between URLs and rendering logic
Listening Mechanism hashchange event Sensing URL changes
Render Outlet #container The mount point for content replacement

Note the line this.load.bind(this). In addEventListener callbacks, this defaults to the element that triggered the event (here, window), not the HashRouter instance. bind returns a new function with the correct this bound—this is a classic technique for handling the loss of this context in JavaScript.

6. Pros and Cons of Hash Routing

Pros

Cons

These shortcomings led to the later, more advanced History routing (based on the pushState / popstate API), but that's a topic for another article.

7. Summary

The core evolution from multi-page applications to single-page applications has a very clear thread:

Multi-Page (Full Page Refresh)
    ↓ Pain point: White screen, redundant rendering, poor experience
Hash Routing (Partial Replacement)
    ↓ Principle: Hash changes don't trigger requests + hashchange event
SPA (Single Page Application)
    ↓ Framework encapsulation: React Router / Vue Router
Modern Front-End Routing

Once you understand hashchange and the core mechanism of "the URL changes but the page doesn't refresh", when you go to learn React Router's <HashRouter> or Vue Router's createWebHashHistory, you'll find that what they do under the hood is exactly the logic we hand-wrote today—just wrapped more elegantly and with more features by the framework.

All complex frameworks, when taken apart, are just plain native APIs. That's the meaning of learning the principles.