跪拜 Guibai
← Back to the summary

How Hash Routing Fixed the Web's White-Screen Problem

The Evolution of Frontend Routing: From White-Screen Refreshes to SPAs — Understand It by Writing a Hash Router Yourself!

Full-text guide: This article starts from the underlying principles of how a browser accesses a URL, step-by-step dissects the pain point of "white-screen refreshes" in traditional web applications, and introduces the core solution of Single-Page Applications (SPAs) — frontend routing. We will dive deep into the implementation details of Hash routing and guide you through writing a complete routing class, thoroughly understanding the principles behind it and the classic JavaScript this binding problem.

1. It All Starts with a URL

Imagine you type www.baidu.com into the browser's address bar and press Enter. What happens behind the scenes?

  1. DNS Resolution: Resolves the domain name www.baidu.com into the server's IP address.
  2. Establish Connection: The browser establishes a connection with the server via the TCP/IP protocol.
  3. Send Request: The browser sends an HTTP request to the server, saying "I want to get the resource at the root path /."
  4. Server Response: The server finds the corresponding resource (e.g., index.html), wraps it as the body of an HTTP response (text/html), and sends it back to the browser.
  5. Browser Rendering: The browser receives the HTML code, starts parsing, builds the DOM tree, loads CSS and JavaScript, and finally renders a beautiful page for the user.
  6. History Record: Finally, this access record is inserted into the browser's history stack.

The core of all this is the URL (Uniform Resource Locator). It acts like a universal "address plate," allowing the client (browser) to find and retrieve specific resources on the server.

sequenceDiagram
    participant U as User
    participant B as Browser
    participant S as Server

    U->>B: 1. Enter URL www.baidu.com
    B->>S: 2. Initiate HTTP request (GET /)
    S-->>B: 3. Respond with HTML document (index.html)
    B->>B: 4. Parse HTML, render page
    B->>B: 5. Insert a history record
    B-->>U: 6. Display the complete page

2. The "White-Screen Pain" of Traditional Websites

In the Web 1.0 and early Web 2.0 eras, most of our websites were Multi-Page Applications (MPA).

For example, consider these two simple pages:

<!-- index.html -->
<nav>
  <!-- target="_blank" means open the link in a new window or tab -->
  <a href="/index.html" target="_blank">Home</a>
  <a href="/about.html" target="_blank">About Us</a>
</nav>
<main><h1>Homepage</h1></main>
<!-- about.html -->
<nav>
  <a href="/index.html">Home</a>
  <a href="/about.html">About Us</a>
</nav>
<main><h1>About Us</h1></main>

💡 Tip: target="_blank" is an attribute of the HTML <a> tag. Its purpose is to tell the browser to open the link in a new window or a new tab. The benefit is that users can access new content without closing the current page, making it suitable for jumping to external sites or temporarily viewing other pages. However, in a multi-page application, if every page opens this way, the user's tabs will quickly pile up, actually increasing management burden.

When we click the "About Us" link, the browser requests a completely new about.html file from the server. During this process:

Core Pain Point: To update a small piece of content on the page, we have to reload the entire page. This is a huge waste of performance and a loss of user experience.

Comic storyboard showing AI painting prompt generation process.png

3. The "No-Refresh" Revolution of Single-Page Applications (SPA)

In pursuit of an app-like smooth experience, the concept of Single-Page Applications (SPA) emerged.

The core idea of SPA: Load all the HTML, CSS, and JavaScript required for the entire application on the first load. Subsequent "page navigations" are essentially just dynamically replacing and rendering content within the same page using JavaScript.

Why Seek Inspiration from Apps?

Have you ever noticed the difference in usage habits between phones and computers?

SPA brings exactly this "native app-like" experience to the Web!

The benefits are obvious:

However, SPA introduces a fatal problem: The URL doesn't change!

No matter how you switch "pages," the address bar always shows www.example.com/index.html. This leads to:

  1. Page Refresh: Once the user manually refreshes, the browser requests index.html again, all state is lost, and it returns to the initial state.
  2. Sharing and Bookmarks: It's impossible to directly link to the "About Us" page via URL.
  3. Browser Navigation: The forward and back buttons become useless.

Phone vs Computer comparison image generation prompt.png

🔍 Interlude: The Trail of Thought — From DOM Programming to the Inspiration of Hash

In the previous section, we discussed the need to move from multi-page to single-page and the specific application scenarios. So, how do we implement an SPA? We can analyze that the core contradiction SPA faces is: We need to change the URL, but we cannot trigger a page refresh. How exactly do we resolve this contradiction? Let's follow a natural path of reasoning to deduce how the experts found the answer step by step. This process is more valuable than just looking at the result.

Step 1: Prevent Default Navigation with DOM Programming

Since the page refresh is caused by clicking a link triggering the browser's default behavior, we simply use JavaScript to prevent it!

document.querySelector('a').addEventListener('click', function(event) {
  event.preventDefault(); // Prevent default navigation
  // Then manually modify the page content
  document.querySelector('main').innerHTML = '<h1>About Us</h1>';
});

This indeed achieves updating content without refreshing the page, a huge improvement in experience!

Step 2: A New Pain Point is Born

But a problem arises: The URL in the address bar hasn't changed; it's still /index.html.

This is awkward—the one-to-one correspondence between URL and resource is broken!

The most fundamental convention of the Web—one URL corresponds to one resource—has been destroyed by us.

Step 3: Is There a Way to Change Only the URL Without Refreshing the Page?

At this point, we start thinking: "Can we make the URL change, but tell the browser not to refresh?"

You might think of:

So, is there a part that, when changed, the browser will "turn a blind eye," neither refreshing nor sending a request?

Step 4: Hash Takes the Stage!

At this moment, we recall a part of the URL that has always existed but is often ignored—the Hash (the # symbol and the content after it).

https://example.com/index.html#/about
                                    ↑
                                 This is it!

Hash's natural characteristics perfectly fit our needs:

  1. Modifying the Hash does not trigger a page refresh — Solves the "no refresh" requirement ✅
  2. Hash changes trigger the hashchange event — We can listen for changes ✅
  3. Hash is saved in the browser's history — Forward/Back buttons work ✅

In this way, the URL changes (#/about), the resource changes ("About Us" is rendered via JS), but the page does not refresh! The correspondence between URL and resource is re-established in a "frontend self-sufficient" manner.

This is the complete thought trajectory behind the birth of Hash routing! It wasn't a spur-of-the-moment idea, but a natural answer found by starting from the simple idea of "using DOM to prevent navigation," discovering the pain point of an unchanging URL, and following the clues.


4. Hash Routing: A Clever "Deception"

With the above reasoning as a foundation, looking at the specific implementation of Hash routing becomes crystal clear.

What is a URL Hash?

https://www.example.com/user/profile?tab=edit#/settings
└─────────────────┬────────────────┘ └────┬────┘ └─┬─┘
                   Hostname/Path         Query String  Hash

Phone vs Computer comparison image generation prompt (1).png

Based on these features, Hash routing became the first-generation standard solution for implementing frontend routing in SPAs.

1. Anchor Links: The "Close Relative" of Hash

A traditional use of Hash is anchor links. In content-rich long pages, we can click a link to make the page "jump" to a specified location.

<!-- Define an anchor -->
<a name="top"></a>
<!-- ... lots of content ... -->
<!-- Clicking this scrolls the page to the top -->
<a href="#top">Back to Top</a>

You might notice that when clicking this link, the URL changes, but the page doesn't refresh. This is exactly the "gene" we need. Except, we no longer use it to control scrolling, but to control the rendering of the entire page's content.

2. Implementing a Simple Hash Router

Let's get our hands dirty and implement a minimal Hash router. Its responsibilities are clear:

  1. Provide a register method, allowing the outside world to map "paths" to "render functions."
  2. Listen for the hashchange event, and when the path changes, find the corresponding render function and execute it.
class HashRouter {
  constructor() {
    // Store routing rules: { '/home': () => {...}, '/about': () => {...} }
    // Note: We uniformly use the format with a leading slash, like '/about'
    this.routes = {};
    // Listen for hash changes
    // Note: Why use bind here? We will explain in detail in the next section!
    window.addEventListener('hashchange', this.load.bind(this));
  }

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

  // Load the page content corresponding to the route
  load() {
    // Get the current URL's hash and remove the leading '#'
    // Note: We uniformly use the format with a leading slash, like '/about'
    const hash = window.location.hash.slice(1) || '/';
    // Get the corresponding handler function from the routing table
    const handler = this.routes[hash];
    if (handler) {
      handler(); // Execute rendering
    } else {
      console.warn(`Route ${hash} is not defined`);
    }
  }
}

// ---------- Usage ----------
// 1. Create a router instance
const router = new HashRouter();

// 2. Get the container
const container = document.getElementById('container');

// 3. Register routes (Note: all paths start with a slash!)
router.register('/', () => {
  container.innerHTML = '<h1>🏠 Home</h1>';
});

router.register('/about', () => {
  container.innerHTML = '<h1>👤 About Us</h1>';
});

router.register('/products', () => {
  container.innerHTML = '<h1>📦 Products</h1>';
});

// 4. When the page loads, manually execute load once to match the initial URL
window.addEventListener('load', router.load.bind(router));

Corresponding HTML structure:

<header>
  <nav>
    <ul>
      <!-- Note: Link addresses are all changed to hash, uniformly starting with a slash -->
      <li><a href="#/">Home</a></li>
      <li><a href="#/about">About Us</a></li>
      <li><a href="#/products">Products</a></li>
    </ul>
  </nav>
</header>
<!-- Content will be dynamically rendered here -->
<main id="container"></main>

⚠️ Must-read for beginners: The slash in a Hash path is not decoration!

Many newcomers to Hash routing step on this landmine: #about and #/about are two completely different Hash values!

location.hash = '#about';   // hash value is '#about'
location.hash = '#/about';  // hash value is '#/about'
// The two are not equal!

If you write router.register('/about', ...) when registering the route, but the link in your HTML is <a href="#about">, they will never match!

Solution: Unify the convention!

  • It is recommended to always use the format with a leading slash, like #/home, #/about, #/user/profile
  • This better matches the semantics of URL paths and looks cleaner
  • Keep it consistent in your code: write '/about' when registering, and #/about in links

Phone vs Computer comparison image generation prompt (2).png

5. Deep Dive into this: The "Commander" in Event Listeners

In the constructor of HashRouter, there is a crucial detail:

window.addEventListener('hashchange', this.load.bind(this));

If we didn't use .bind(this) and instead wrote this.load directly, what would happen? The program would throw an error!

This is a very classic this binding problem in JavaScript.

The binding of this in JavaScript depends on how the function is called, not where it is defined.

  1. Who calls load?

    • When the hashchange event fires, it is the browser (specifically the window object) that calls the load function. The invocation is similar to window.load().
  2. Where does this point at this time?

    • Since the caller is window, according to the rule of "whoever calls it, this points to them," the this inside the load function defaults to pointing to the window object.
  3. Where is the problem?

    • Our load method has a line: const handler = this.routes[hash];.
    • If this points to window, then this.routes is window.routes. But our routes is attached to the HashRouter instance, so window.routes is naturally undefined. Thus, the program throws an error.
  4. What does .bind(this) do?

    • bind is a native method of functions. It creates a new function. The this of this new function is permanently bound to the first argument of bind.
    • In this.load.bind(this), the first this is the instance of HashRouter. So, bind creates a new load function and forcibly specifies that its this will forever point to our HashRouter instance.
    • Thereafter, no matter who calls this new function or how it is called (e.g., called by window), its this will always be the instance we specified.

Understanding with an analogy:

Therefore, .bind(this) is the key guarantee that we can correctly access this.routes.

6. Summary and Outlook

Pros and Cons of Hash Routing

The Road Ahead: History API

To solve these problems of Hash routing, HTML5 introduced the History API. It provides the pushState and replaceState methods, which can directly modify the path part of the URL (/home, /about) without triggering a page refresh, and combined with the popstate event, enable more elegant and "real" frontend routing.

// Change the URL to /about without refreshing the page
history.pushState({page: 'about'}, 'about', '/about');

The History API is the underlying foundation of today's popular frontend routing libraries like Vue Router (history mode) and React Router (BrowserRouter). It makes URLs as clean as traditional websites, paving the way for SPA SEO optimization and a more perfect user experience.

Final Words

Starting from browser principles, moving through the pain points of MPA, to the birth of SPA, and finally implementing a classic Hash router while thoroughly understanding the this binding problem within it. This is not just an implementation of a router, but a deep understanding of the evolution of web application architecture.

I hope this article helps you build a solid foundation. Once you understand Hash routing, when you look at the more complex History API routing, you'll find the underlying ideas are the same.


👍 If this article was helpful, please give it a triple-tap!

Your support is the greatest motivation for me to continue producing high-quality technical articles!

Next up: We will dive deep into the History API, guiding you to hand-write a more perfect BrowserRouter, completely bidding farewell to the # symbol. Stay tuned! 🚀