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:
- White Screen Flash: If the network is slightly slow, the page flashes white before new content appears. For mobile users, this experience is disastrous.
- Redundant Transmission: The navigation bar, footer, logo, style files... these common resources are re-transmitted and re-rendered every time, wasting significant bandwidth.
- State Loss: After a page jump, the previous scroll position, form input content, playback progress, etc., are all lost.
- Server Pressure: Every request consumes server computing resources to assemble a complete HTML page.
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:
- The URL changes (from
/#/page1to/#/page2) - But the page does not refresh
- Common parts like the navigation bar and footer remain unchanged
- Only the content inside
<div id="container">is replaced
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:
The
hashchangeevent: 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.this.load.bind(this): This is a classic JavaScriptthisbinding problem. When thehashchangeevent fires,thisinside the event handler defaults to pointing to the object that triggered the event —window. But we wantthisto point to the HashRouter instance so we can accessthis.routers.bind(this)creates a new function wherethisis permanently locked to the current instance, no matter who calls this function in the future.The related methods
call,apply, andbindare the three weapons for 'manually specifyingthis' in JavaScript functional programming.callandapplyare for temporary borrowing — execute immediately and changethis;bindis for permanent binding — returns a new function withthisbound 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:
History API (HTML5):
pushStateandreplaceStateallow developers to change the path part of the URL (not just the hash) without refreshing the page, achieving 'clean URLs' likeexample.com/page1.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.
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.
Nested Routes and Dynamic Routes:
/products/:idcan 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:
- Multi-Page (MPA) is the web's native model, with a one-to-one correspondence between URLs and HTML files. It's simple but requires a full page refresh on every switch.
- The original purpose of Hash is in-page anchor navigation; changing the hash does not trigger a page refresh, a characteristic creatively exploited by developers.
- Hash Routing, through
hashchangeevent listening + routing table mapping + dynamic DOM replacement, achieves a 'page switching' effect without refreshing the page — this is the cornerstone of SPAs. bind/call/applyare three ways to manually specifythisin JavaScript; understanding them is crucial for reading any front-end framework source code.- Today's React Router and Vue Router are not fundamentally different in concept from this small demo — they just use more robust methods to engineer the 'route mapping + DOM replacement' pattern.
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.