跪拜 Guibai
← Back to the summary

The 60-Line Hash Router That Explains Every Modern Front-End Framework

Have you ever noticed: when you open Taobao, browse Bilibili, or read Zhihu, page transitions are always smooth and seamless; but occasionally on some older websites, every time you click a link the page 'flashes white' and reloads. The secret behind this is front-end routing — the evolution from traditional multi-page architecture to modern single-page applications. This article uses two hands-on, runnable demo projects to help you thoroughly understand this piece of tech history.


1. Back to Basics: How Does a Browser 'See' a Web Page?

Before discussing routing, let's return to the most fundamental question: what happens when you type a URL into the browser's address bar and press Enter?

The essence of this process is a relay race:

1. URL Parsing       ← The browser breaks down the address you entered (protocol, host, path, parameters...)
2. HTTP Request      ← The browser sends a request to the server: 'Please give me /index.html'
3. Server Response   ← The server runs backend programs, generates HTML text, and returns it to the browser
4. Browser Rendering ← The browser receives the HTML, builds the DOM tree, and paints pixels on the screen
5. History Insertion ← The browser inserts a new record into the browsing history (for 'forward/back' navigation)

The core of all this is the correspondence between URL and resource — one address corresponds to one HTML page. This model has been established since the birth of the World Wide Web in the 1990s and is known as the cornerstone of the 'interconnected web': with just a link, you can jump to any corner of the internet.


2. The demo/ Folder: The 'Setting Sun' of Traditional Multi-Page

Look at the two HTML files in the demo/ folder. They form a minimalist traditional multi-page website:

index.html (Homepage):

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

about.html (About Page) has an almost identical structure, only the title is changed to 'About Us'. The navigation bar is exactly the same on both pages — in the traditional model, every page must write out the common parts like the navigation bar and footer individually.

When you click the 'About Us' link, the browser executes the full set of operations: initiates a new HTTP request → the server returns a complete HTML again → the entire page re-renders. This is called an MPA (Multi-Page Application).

Pain Points of the Traditional Model

In the PC internet era, the multi-page model ran for decades without major issues. But in the mobile internet era, this 'full page refresh' design became a performance killer:

This leads to the core contradiction: On today's devices, is there a way to replace only the changed parts without re-rendering the entire page?


3. demo2/demo.html: The 'Past Life' of Hash — Anchor Links

Before discussing front-end routing, let's look at demo2/demo.html, which demonstrates the original use of hash — anchor links.

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

Here, #bottom and #top are hashes — the part of the URL after the # sign. Its original purpose is in-page navigation: clicking 'Go to Bottom' makes the browser automatically scroll to the location of <a name="bottom">, like taking an elevator directly to a specific floor.

Key Feature: When the hash changes, the browser does not re-request the server, nor does it refresh the page. The page is still the same page, only the scroll position has changed.

Now look at the script part:

window.addEventListener('hashchange', function(event) {
  console.log('hash changed');
  console.log(event.newURL);  // The new URL (the hash part changed)
  console.log(event.oldURL);  // The old URL
})

The browser provides a hashchange event — whenever the part after # changes, this event is triggered. It is precisely this mechanism that made developers keenly realize: If the hash is used as a 'page identifier' rather than a 'position anchor', couldn't we switch content without refreshing the page?

A brilliant technical idea was born from this.


4. demo2/index.html: The 'Groundbreaking' Hash Routing

Look at demo2/index.html. It uses less than 60 lines of JavaScript to manually implement a complete front-end routing system. This represents the critical leap from multi-page to single-page applications.

Page Structure: Revolutionary in its Minimalism

<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>

Notice the change in links here — no longer the traditional href="index.html", but href="#/page1". When you click these links:

This is the core idea of SPA (Single Page Application): The entire website is just 'one page', simulating page jumps by dynamically switching DOM content.

The HashRouter Class: A Handcrafted Routing Engine

class HashRouter {
  constructor() {
    this.routers = {};  // Routing table: hash → callback function
    window.addEventListener('hashchange', this.load.bind(this));
  }

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

  load() {
    let hash = location.hash.slice(1);  // Get the part after # (remove #)
    let handler = this.routers[hash];   // Find the corresponding handler from the routing table
    handler.call(this);                  // Execute the handler
  }
}

This forty-plus-line class condenses the most primitive and core logic of front-end routing. Let's dissect it layer by layer:

Layer 1: The Routing Table this.routers

this.routers is a plain JavaScript object, essentially a key → value mapping:

{ "/page1": callback1, "/page2": callback2, "/page3": callback3 }

This is conceptually identical to a traditional backend routing table — except the traditional backend route returns an entire HTML page, while the front-end route returns a callback function that, when executed, updates a specific DOM area.

Layer 2: The hashchange Event Listener

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

There are three layers of technical detail worth digging into here:

  1. The hashchange event: The browser automatically triggers this when the hash changes. This saves us from writing our own polling to check if the URL has changed — handing the initiative to the browser.

  2. this.load.bind(this): This is a classic JavaScript this binding problem. When the hashchange event fires, this inside the event handler defaults to pointing to the object that triggered the event — window. But we want this to point to the HashRouter instance so we can access this.routers. bind(this) creates a new function where this is permanently locked to the current instance, no matter who calls this function in the future.

  3. The related methods call, apply, and bind are the three weapons for 'manually specifying this' in JavaScript functional programming. call and apply are for temporary borrowing — execute immediately and change this; bind is for permanent binding — returns a new function with this bound but does not execute immediately.

Layer 3: The load() Method — The Core of Route Dispatching

load() {
  let hash = location.hash.slice(1);  // e.g., "/page1"
  let handler = this.routers[hash];   // Look up from the routing table
  handler.call(this);                  // Execute, ensuring `this` is correct inside the callback
}

location.hash is a browser-provided API that returns the hash part of the current URL (including #). slice(1) removes the first character #, yielding /page1.

Then it looks up the corresponding handler from the this.routers object — this is essentially an O(1) hash lookup, extremely efficient.

Finally, handler.call(this) ensures that if this is used inside the handler, it points to the HashRouter instance, not the global object.

Layer 4: Registration and Startup

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');

The registration phase binds the three paths to their handler functions one by one. Here, the handlers simply modify the text content of #container, but in a real project, the handler can do any complex operation — initiate an AJAX request to fetch data, render an entire component tree, update the page title...

Why Is This Called 'Groundbreaking'?

This few-dozen-line demo solves all the core pain points of traditional multi-page:

Traditional MPA SPA + Hash Routing
Full page refresh on every switch, white screen flash Only replaces the changed area, no white screen
Common parts (nav bar, etc.) re-render every time Common parts remain unchanged
Page state is lost State is always kept in memory
Server returns complete HTML every time Server only returns data (JSON), rendering is done by the front end
Browser downloads lots of duplicate resources Only transmits changed data

The correspondence between URL and resource is fully preserved — each hash value corresponds to the content of a 'virtual page' — but the implementation method changed from 're-request the entire page' to 'partially replace the DOM'.


5. From Front-End Routing to Modern Frameworks: The Story After

The HashRouter in the demo is the 'Stone Age' implementation of front-end routing. But the core ideas it embodies — routing table mapping, event-driven, dynamic DOM replacement — are still the foundation of modern routing libraries like React Router and Vue Router.

The subsequent development path is roughly as follows:

  1. History API (HTML5): pushState and replaceState allow developers to change the path part of the URL (not just the hash) without refreshing the page, achieving 'clean URLs' like example.com/page1.

  2. React Componentization: Routing is no longer just 'replacing innerHTML', but mounting and unmounting components. When a route matches a path, the corresponding React component is rendered; unmatched page components do not exist in the DOM tree at all.

  3. Route Lazy Loading: Code for non-current pages doesn't even need to be downloaded initially; it's only dynamically loaded when the user actually visits — significantly improving first-screen speed.

  4. Nested Routes and Dynamic Routes: /products/:id can match any product ID; route parameters are injected into the component, one rule covering countless pages.


6. Summary: Understand the Foundation to Master the Framework

From the traditional <a href="xxx.html"> links in demo/, to the anchor hash in demo2/demo.html, to the handcrafted HashRouter in demo2/index.html — these three demos string together a complete chain of technological evolution.

Core takeaways:

Next time you write <Route path="/about" component={About} /> in a React project, think about that HashRouter with less than 60 lines — the core logic behind it is connected to the line of code you're writing. Understand the foundation, and you can master the framework.