跪拜 Guibai
← All articles
Frontend

Frontend Routing from Scratch: Hash, History, and the MPA-to-SPA Shift

By Jasper_L ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Understanding that React Router is thin sugar over `hashchange` and `location.hash` turns framework routing from memorized incantation into a predictable tool. Developers who know the underlying event loop and `this`-binding mechanics debug navigation bugs faster and make better trade-offs between hash routing, history API routing, and SSR strategies.

Summary

Multi-page applications reload the entire document on every navigation, repeating 90% of the markup for a 10% content change and producing a jarring white flash. Single-page applications solve this by keeping one HTML shell and swapping only the content area, but they must still update the URL for bookmarking and back-button support. The hash fragment—`#/about`—changes the address bar without sending a new HTTP request, making it the foundation of client-side routing. A hand-rolled `HashRouter` class in vanilla JavaScript, wired to the `hashchange` event, demonstrates the mechanism before React Router's `HashRouter`, `Routes`, `Route`, `Link`, `useParams`, `Outlet`, and `lazy`-plus-`Suspense` code-splitting are introduced as declarative wrappers over the same primitives.

Takeaways
MPA navigation triggers a full HTTP request-response cycle, clears the DOM, and re-renders everything, causing a visible white flash.
SPA keeps a single HTML page and replaces only the content area, eliminating the flash and reducing server load to data-only API calls.
The URL hash (`#`) can change without the browser sending a request, which is the mechanism that makes client-side routing possible.
A minimal HashRouter needs a routing table, a `hashchange` listener, and a `load` method that reads `location.hash` and invokes the matching callback.
`addEventListener` callbacks lose the instance's `this` context unless explicitly bound with `.bind(this)`, a common pitfall when wiring class methods.
React Router's `HashRouter` is a declarative wrapper around the same `hashchange` + `location.hash` primitives.
`Link` intercepts clicks to update the hash without a server request, while still rendering a standard `<a>` tag for accessibility.
`lazy` plus `Suspense` splits route components into separate chunks that load only on first visit, addressing SPA's large initial bundle problem.
Conclusions

Frameworks feel like magic only when the underlying browser primitives are unknown; once `hashchange` and `location.hash` are understood, React Router becomes an obvious convenience rather than a black box.

The `this`-binding trap in `addEventListener` is a recurring source of bugs that many developers work around without understanding—explicitly teaching it as part of routing makes the lesson stick.

SPA's SEO and initial-load weaknesses are not solved by hash routing; they require SSR or pre-rendering, which the article acknowledges but does not implement, leaving a natural next step for the reader.

Concepts & terms
Hash Routing
A client-side routing technique that uses the URL fragment (`#`) to simulate page navigation. Because changing the hash does not trigger an HTTP request, JavaScript can listen for `hashchange` events and swap DOM content without a full page reload.
Single-Page Application (SPA)
A web application that loads a single HTML document and dynamically updates the view as the user interacts, using JavaScript to replace content in-place rather than fetching new pages from the server.
Multi-Page Application (MPA)
The traditional web model where each user navigation triggers a full HTTP request, the server returns a complete HTML page, and the browser re-renders the entire document.
Code Splitting (lazy + Suspense)
A technique where JavaScript bundles are split into smaller chunks loaded on demand. React's `lazy` function defers component loading until first render, and `Suspense` displays a fallback UI while the chunk downloads.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗