跪拜 Guibai
← Back to the summary

SPA Routing from Scratch: Why the Hash Fragment Powers Every Single-Page App

1. How Browsers Access Web Pages

The instructor first wrote the following in the readme:

## Route
- After entering a URL, the browser acts as the user agent
- It initiates a request to the server via the HTTP protocol
- The server, in a listening state, responds to the browser with text/html
- The browser takes the response data and renders the page
- A record is inserted into the browsing history

This is the most traditional browsing model. A user enters a URL in the address bar, and the browser acts as a "user agent," initiating a request to the server via the HTTP protocol. Upon receiving the request, the server is in a "listening" state and returns a text/html response. The browser gets the HTML data, renders the page, and simultaneously inserts a record into the browsing history.

Every web developer is familiar with this process, but it has a hidden problem.


2. The White Screen Pain Point of Traditional Multi-Page Apps

The readme continued:

## Links
The web is connected by links
<a href="https://www.baidu.com"></a>
Besides navigation, what else?
Traditionally, the entire page must be re-rendered every time, which is slow and unnecessary.

The instructor opened two files in the demo directory to give us an intuitive feel for how traditional multi-page apps work.

demo/index.html:

<li><a href="http://127.0.0.1:5500/.../index.html">Home</a></li>
<li><a href="http://127.0.0.1:5500/.../about.html">About Us</a></li>

These two pages use full server URLs to navigate between each other. Every time a link is clicked, the browser initiates a brand new HTTP request, the server returns the entire HTML page, and the browser re-renders all the content.

What's the problem? If the network is a bit slow, the page will "flash white." This happens because the entire page is reloading—the navigation bar, footer, and layout, which haven't changed, are downloaded, parsed, and rendered all over again. The readme states:

Traditional multi-page apps require a full re-render every time. In the mobile era, this is unnecessary; the page might flash white (if the network is a bit slow).

Moving from the PC era to the mobile era, user expectations have changed. Switching pages within an app never causes a "white screen flash." Can the web achieve the same?


3. The SPA Philosophy: One Page, Dynamically Swapping Content

The readme provided the answer:

Single Page Application
SPA
How to display rich content within a single web page
DOM Programming

SPA (Single Page Application) core concept: instead of fetching a whole new page of HTML from the server for every navigation, load the page only once, and handle all subsequent content switching by manipulating the DOM with JavaScript.

The readme captured the essence of SPA routing with two lines of pseudocode:

Improved user experience
Based on the corresponding URL
/index.html content DOM placed into #container
/about.html content DOM placed into #container

Two key concepts emerge here. The first is #container—a "mount point" within the page, an empty DOM element where all page content switching occurs. The second is deciding what content to place based on the URL—different URLs mean different DOM is inserted into the mount point. These two capabilities together form the basis of SPA routing.


4. The Key Problem: How to Change the URL Without Sending a Request?

An SPA needs the URL to change (because different URLs correspond to different resources, and the user's browsing history, forward, and back buttons must work correctly), but it must absolutely not initiate a new HTTP request (because a request triggers a full page refresh).

The readme raised this contradiction:

## Single Page Also Has
- Clicking links to navigate
- A one-to-one correspondence between URLs and resources
It's not just DOM programming
How to change the URL
The hash method can do this
Changing the hash changes the URL, but it won't resend a request and won't navigate away.

The answer is hash.


5. The Five-Part URL and the Special Status of Hash

The readme drew out the complete structure of a URL:

## Hash Routing
http(s)://www.baidu.com/u/123?a=1&b=2#page1
protocol    host      path  queryString  hash
Part Name Function Does changing it send a request?
https:// protocol Protocol Yes
www.baidu.com host Host (domain) Yes
/u/123 path Path Yes
?a=1&b=2 queryString Query parameters Yes
#page1 hash Hash/Anchor No

The unique aspect of hash is that it is not sent to the server. When a browser makes an HTTP request, the # and everything after it in the URL is "withheld." The server is completely unaware of the hash's existence. Therefore, modifying the hash does not trigger any network request, and the page does not refresh.

This is the breakthrough for SPA routing.

A question that arises here: Besides routing, what else can a hash do? What is the difference between the name and href attributes of an <a> tag?

Answer: The original purpose of a hash is "anchor positioning"—marking a location within a long page so that clicking a link takes you directly there, like an express elevator. <a name="top"> sets a signpost (a destination) somewhere on the page, while <a href="#top"> is a jump point (a starting point) that navigates to that signpost. One marks the spot, the other jumps to it; they work as a pair. SPA front-end routing is a creative repurposing of this mechanism—the browser thinks you're jumping to an anchor, but you're actually switching pages.


6. Demo Verification: Anchor Positioning and the hashchange Event

The instructor opened demo2/demo.html to verify two fundamental capabilities of hash with code.

Part 1: Anchor Positioning (Browser behavior, zero JS)

<a name="top"></a>                          <!-- Set an anchor at the top of the page -->
<a href="#bottom">Go to bottom</a>          <!-- Clicking changes the hash to #bottom, page scrolls down -->
<div style="height: 200vh;background-color: yellow;"></div>
<div style="height: 300vh;background-color: red;"></div>
<a href="#top">Back to top</a>              <!-- Clicking changes the hash to #top, page scrolls back up -->
<a name="bottom"></a>                       <!-- Anchor at the bottom -->

This is pure browser behavior, requiring no JavaScript. Clicking "Go to bottom" appends #bottom to the URL, and the browser automatically scrolls to the position of <a name="bottom">. No request is sent, the page doesn't refresh, but the browsing history is recorded as usual, and the forward/back buttons work correctly.

Part 2: The hashchange Event (JS steps in to observe)

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

hashchange is a native event provided by the browser. Whenever the part of the URL after the # changes, the browser fires this event. event.newURL and event.oldURL carry the complete URLs before and after the change, respectively.

These two parts together answer the two fundamental questions of SPA routing:

  1. How to change the URL without a refresh? — Use hash.
  2. How to know the URL changed? — Listen for the hashchange event.

With these two basic capabilities, the conditions for writing a HashRouter by hand are all in place.


7. Hand-Writing a HashRouter: The Three-Piece Suite of SPA Routing

The instructor opened demo2/index.html, the core file for today's lesson.

7.1 Page Structure

<header>
  <nav>
    <ul>
      <li><a href="#/page1">Home 1</a></li>    <!-- hash link -->
      <li><a href="#/page2">Page 2</a></li>
      <li><a href="#/page3">Page 3</a></li>
    </ul>
  </nav>
</header>
<main></main>
<footer></footer>
<div id="container"></div>    <!-- Mount point: empty, to be filled by JS -->

The header, main, and footer are the "page shell" that never changes. <div id="container"> is the mount point—initially empty, all page content is dynamically injected here. The href attributes of the three <a> tags all start with #, ensuring that clicks only change the hash and do not send a request.

7.2 The HashRouter Class: constructor

class HashRouter {
  constructor() {
    // this points to the instance
    this.routers = {}       // Routing table; with separated front and back ends, the front end needs its own independent routes
    window.addEventListener('hashchange',
      this.load.bind(this)  // bind returns a new function
    )
  }
}

The constructor does two things.

First: Creates a routing table this.routers = {}. In traditional multi-page development, routing rules are on the back end (different URL paths correspond to different server files). After separating the front and back ends, the front end also needs its own routing table—which is just a plain JavaScript object where the key is the hash path and the value is the corresponding handler function.

Second: Listens for the hashchange event. The line this.load.bind(this) is worth dissecting carefully.

addEventListener has a default behavior: the this inside the callback function is set to the DOM element that triggered the event (in this case, window). If bind(this) were not used, the this inside the load method would point to window, not the HashRouter instance—and thus this.routers would be inaccessible.

The purpose of bind(this) is to return a new function where the internal this is permanently locked to the HashRouter instance, no matter how the browser tries to change it. This differs from call and applycall and apply execute the function immediately, whereas bind returns a new function. An event listener needs a "function to be called later," so only bind will work.

A question that arises here: Is the this in bind(this) the same as the this in this.routers? If there are multiple new HashRouter() instances, will their this.routers interfere with each other? What data structure is this.routers essentially?

Answer: All this references inside the constructor are the same thing—the specific instance object being created by new. this.routers attaches a property to this instance, and bind(this) locks the load method to the same instance. Each new call creates a brand new object, each with its own independent routers, just like each person has their own phone with a contact list that doesn't mix with others'. this.routers is essentially a plain JavaScript object, storing data in a key-value format where the key is a hash path string and the value is a callback function.

7.3 register: Registering a Route

register(hash, callback) {
  this.routers[hash] = callback
}

register does just one thing: stores a record in the routing table. hash is the key (e.g., '/page1'), and callback is the content behind the door that the key opens.

Calling it three times in a row:

let router = new HashRouter()
let container = document.getElementById('container')
router.register('/page1', () => container.innerHTML = 'Page 1')
router.register('/page2', () => container.innerHTML = 'Page 2')
router.register('/page3', () => container.innerHTML = 'Page 3')

After these three calls, this.routers becomes:

{
  '/page1': () => container.innerHTML = 'Page 1',
  '/page2': () => container.innerHTML = 'Page 2',
  '/page3': () => container.innerHTML = 'Page 3',
}

Each key corresponds to an arrow function. These arrow functions do not execute right now; they are just stored and will only be called when the corresponding hash is matched. This is the meaning of a callback: it's not executed now, but "called back when needed later."

7.4 load: Look Up and Execute

load() {
  console.log(this)                         // Verify that 'this' is the HashRouter instance
  let hash = window.location.hash.slice(1)  // '#/page1' → '/page1'
  let handler
  if (!hash) {
    // hash is empty (page just opened), do nothing
  } else {
    handler = this.routers[hash]            // Retrieve the callback from the routing table
  }
  handler.call(this)                        // Execute the callback
}

The execution steps of the load method:

  1. Get the hash part of the current URL via window.location.hash, resulting in a string like '#/page1'.
  2. Use slice(1) to remove the first character #, yielding '/page1'—this value is exactly the key registered in the routing table.
  3. Use this key to look up this.routers and retrieve the corresponding callback function.
  4. Execute it via handler.call(this). The effect is container.innerHTML = 'Page 1', and new content appears in the mount point.

The !hash empty-check is also important: when a user first opens the page and hasn't clicked any links yet, the hash is an empty string, and nothing can be found in the routing table. This check prevents errors caused by a null value.


8. The Complete Flow, Connected

When all this code is combined, a complete SPA page-switching flow looks like this:

User clicks <a href="#/page2">Page 2</a>
  ↓
Browser modifies the URL: index.html → index.html#/page2
  ↓
No HTTP request is sent (because only the hash changed)
  ↓
Browser fires the hashchange event
  ↓
load() is called
  ↓ hash = '/page2'
  ↓ handler = this.routers['/page2']  → get the callback
  ↓ handler.call(this)  → execute the callback
  ↓ container.innerHTML = 'Page 2'
  ↓
The text "Page 2" appears inside <div id="container"> on the page
  ↓
The navigation bar and footer remain untouched; only the mount point's content is replaced

The entire process involves zero refreshes and zero requests. The user completes a "page switch" within the same page, with an experience as smooth as a native app.


9. Summary: The Three-Piece Suite of SPA Hash Routing

Today's learning unfolded along a clear path:

  1. Traditional Browsing Model: URL → HTTP request → Server returns HTML → Full page render → White screen flash

  2. SPA Philosophy: One page, dynamically replacing the DOM, no need to repeatedly request the server

  3. Hash Mechanism: The part of the URL after # is not sent to the server; changing the hash does not refresh the page

  4. Anchor Positioning: The original purpose of hash—"express elevator" navigation within a long page

  5. hashchange Event: A native browser listening mechanism that automatically fires when the hash changes

  6. Hand-Written HashRouter: A three-piece combination—hash links + mount point + hashchange listener

    • this.routers = {}: Front-end routing table (a key-value object)
    • register(hash, callback): Registers a routing rule
    • load(): Looks up the table → retrieves the callback → executes it → replaces the DOM
    • bind(this): Locks the this reference, ensuring instance properties are accessible within the event callback

Once you've mastered a hand-written HashRouter, you'll find that when learning React Router later—React's <Link> is just an <a> tag that encapsulates hash modification, <Route> is a declarative way to register routes, and <Routes> is a container that matches a path and renders an element. The underlying logic is completely identical to the few dozen lines of code written by hand today.