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
thisbinding 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?
- DNS Resolution: Resolves the domain name
www.baidu.cominto the server's IP address. - Establish Connection: The browser establishes a connection with the server via the TCP/IP protocol.
- Send Request: The browser sends an HTTP request to the server, saying "I want to get the resource at the root path
/." - 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. - 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.
- 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:
- Homepage (index.html)
- About Us (about.html)
<!-- 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:
- The page goes through the full cycle of "unload → request → parse → load → repaint."
- The screen will flash blank for a moment, which is what we often call the "white screen".
- Although modern browsers and servers are very performant, once page resources are large or the network is poor, the stuttering sensation of this experience becomes very noticeable.
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.
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?
- Large computer screen: We can open a dozen browser tabs simultaneously, each corresponding to a different website, switching between them with ease.
- Small phone screen: Managing browser tabs on a phone is far less convenient than on a computer. Imagine if every link you clicked on a mobile webpage opened a new tab—the top tab bar would quickly become densely packed, and switching would be a finger-cramping nightmare. Therefore, mobile apps almost all provide a "single-page" experience—completing all operations within one interface, switching content via bottom tabs or side swipes, smoothly and without stutter.
SPA brings exactly this "native app-like" experience to the Web!
The benefits are obvious:
- Extremely Smooth: No page refreshes, no white screens.
- Better Experience: Fast switching, more immediate interaction feedback.
- Reduced Server Load: The server only needs to provide data APIs, without being responsible for page rendering (separation of frontend and backend).
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:
- Page Refresh: Once the user manually refreshes, the browser requests
index.htmlagain, all state is lost, and it returns to the initial state. - Sharing and Bookmarks: It's impossible to directly link to the "About Us" page via URL.
- Browser Navigation: The forward and back buttons become useless.
🔍 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!
- A user wants to bookmark the "About Us" page, but the copied link is
index.html. - A user clicks the browser's "back" button, and nothing happens because the URL never changed.
- Refreshing the page returns to the homepage; the "About Us" switch just made is lost.
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:
- Directly changing
window.location.href? ❌ Will refresh. - Using
history.pushState? This came later and wasn't widespread yet. - Changing the URL's query string
?page=about? ❌ Also triggers a request.
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:
- Modifying the Hash does not trigger a page refresh — Solves the "no refresh" requirement ✅
- Hash changes trigger the
hashchangeevent — We can listen for changes ✅ - 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
- Hash is the
#symbol and the string of characters after it, e.g.,#/settings. - Key Feature 1: Modifying the Hash does not refresh the page. When you change
#/settingsto#/user, the browser does not initiate a new network request. - Key Feature 2: Hash changes trigger the
hashchangeevent. This means we can listen for URL changes via JavaScript. - Key Feature 3: Hash is saved in the browser's history. This means the browser's forward and back buttons work normally.
- 🔐 Key Feature 4: Hash is not sent to the server. When the browser makes an HTTP request, the
#symbol and the part after it are discarded; the server never receives the Hash information. Therefore, Hash routing is entirely controlled by the frontend and requires no special server configuration.
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:
- Provide a register method, allowing the outside world to map "paths" to "render functions."
- Listen for the
hashchangeevent, 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:
#aboutand#/aboutare 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#/aboutin links
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.
- Default Binding: When a function is called "standalone" (e.g.,
fn()),thisinside the function points to the global objectwindowin non-strict mode, and isundefinedin strict mode. - Implicit Binding: When a function is called as a method of an object (e.g.,
obj.fn()),thispoints to that object (i.e.,obj).
Who calls
load?- When the
hashchangeevent fires, it is the browser (specifically thewindowobject) that calls theloadfunction. The invocation is similar towindow.load().
- When the
Where does
thispoint at this time?- Since the caller is
window, according to the rule of "whoever calls it,thispoints to them," thethisinside theloadfunction defaults to pointing to thewindowobject.
- Since the caller is
Where is the problem?
- Our
loadmethod has a line:const handler = this.routes[hash];. - If
thispoints towindow, thenthis.routesiswindow.routes. But ourroutesis attached to theHashRouterinstance, sowindow.routesis naturallyundefined. Thus, the program throws an error.
- Our
What does
.bind(this)do?bindis a native method of functions. It creates a new function. Thethisof this new function is permanently bound to the first argument ofbind.- In
this.load.bind(this), the firstthisis the instance ofHashRouter. So,bindcreates a newloadfunction and forcibly specifies that itsthiswill forever point to ourHashRouterinstance. - Thereafter, no matter who calls this new function or how it is called (e.g., called by
window), itsthiswill always be the instance we specified.
Understanding with an analogy:
- Instance: A company (HashRouter instance).
loadmethod: A work plan of the company..bind(this): Stamping this work plan with a "Property of XX Company" seal.- Event Trigger: The browser says, "I want to see this work plan."
- Effect: Because the plan is stamped, when the browser looks at it, it knows this is "XX Company's" work, and all operations inside (like
this.routes) must and can only use "XX Company's" resources.
Therefore, .bind(this) is the key guarantee that we can correctly access this.routes.
6. Summary and Outlook
Pros and Cons of Hash Routing
Pros:
- Excellent compatibility, supported by all browsers.
- Simple to implement, no special server-side configuration required (because Hash is never sent to the server).
Cons:
- The URL contains a
#symbol, which is not aesthetically pleasing. - Not SEO (Search Engine Optimization) friendly, because search engine crawlers typically do not execute JavaScript and cannot crawl content behind the
#.
- The URL contains a
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!
- Like 👍: Let more friends see this content
- Bookmark ⭐: Convenient for future review
- Comment 💬: I will seriously reply to every question you have
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! 🚀