Hash vs. History Routing: What Actually Happens in the Browser
What is routing? Routing is used to describe the path of resources on a server. This article helps us understand front-end routing.
1. Why Understand Front-End Routing?
In traditional multi-page applications, every page switch requires a new HTML request. If the user's network fluctuates significantly, the browser will display a blank screen while switching pages, forcing the user to wait several seconds for rendering. Single-page applications (SPAs) do not refresh the page; instead, they only switch components.
How is a single-page application implemented? Let's use vanilla JS to build a simple one:
<body>
<header>
<nav>Home</nav>
<nav>Hot Topics</nav>
</header>
<div id="root"></div>
</body>
const content = [
'<h2 class="home">Home</h2>',
'<h3 class="hot">Hot Topics</h3>',
]
let navs = document.getElementsByTagName('nav');
Array.from(navs).forEach((nav, index) => nav.addEventListener('click', () => {
document.getElementById('root').innerHTML = content[index];
}))
This creates a simple SPA where clicking navigation manually switches the content inside root, but the URL does not change; it only achieves "component switching."
The core functions of front-end routing are:
- Changing the browser URL
- Not refreshing the page
- Finding and rendering the corresponding component based on the URL
In other words, front-end routing establishes a mapping relationship between browser URL addresses and components.
2. Core Flow of Front-End Routing
- Modify the browser URL without refreshing the page.
- Listen for URL changes.
- Find the corresponding component from the mapping table based on the URL and render it.
We have two methods to implement this: hash routing and history routing.
3. Hash Principle
- What is a hash?
For example:
Here, #/home is the hash.
That is, the browser treats the string following the # in the URL as a hash value.
- Characteristics of hash
- Changes to the content after
#in the URL do not cause a page refresh. - The current hash can be obtained via
location.hash. - The
hashchangeevent can be listened to.
- Hash routing implementation steps
- Define a route mapping table.
- Change the hash on navigation click.
- Listen for
hashchange. - Find the corresponding component based on
location.hash. - Render it to the page.
- Mapping table
const routes = [
{
path: '/home',
component: () => {
return '<h2>Home Page</h2>'
}
},
{
path: '/hot',
component: () => {
return '<h3>Hot Topics Page</h3>'
}
}
]
- Listening for hashchange
// Know that the URL has changed
window.addEventListener('hashchange', () => {
// Find the corresponding path in routes and render the corresponding component
renderView(location.hash)
})
- Render function
function renderView(hashVal) {
hashVal = hashVal.replace('#', '')
let route = routes.find(item => item.path === hashVal)
if(route) {
document.getElementById('root').innerHTML = route.component()
}
}
- Handling the initial load
// Initial page refresh
window.addEventListener('DOMContentLoaded', () => {
renderView(location.hash)
})
- Hash routing summary Advantages:
- Simple to implement
- Good compatibility
- Switching URLs does not refresh the page
Disadvantages:
- URL contains
# - Not aesthetically pleasing
- Not favorable for some SEO scenarios
4. History Routing Principle
- What is history?
History is an object provided by the browser for managing the browser's session history.
- Provided methods
pushState(): Adds an entry to the browser's history stack.popState(): Pops an entry from the browser's history stack.replaceState(): Replaces the top entry in the browser's history stack.
pushState() can modify the URL without triggering a page refresh. Listening to popstate handles the browser's forward and backward navigation events.
5. Handwriting a History Router
- Define the route table
const routes = [
{
path: '/home',
component: () => {
return '<h2>History Home Page</h2>'
}
},
{
path: '/hot',
component: () => {
return '<h3>History Hot Topics Page</h3>'
}
}
]
- Prevent default navigation
e.preventDefault()
- Use pushState to modify the URL
history.pushState(null, '', el.getAttribute('href')) // Enter the browser's cache stack
This step achieves:
- URL change
- No page refresh
- Adds an entry to the browser history
- Manually render the page
renderView(location.pathname)
- Listen for browser forward/backward navigation
window.addEventListener('popstate', () => {
renderView(location.pathname)
})
- Render function
function onLoad() {
// Path change, render corresponding component
let linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault() // Prevent default <a> tag navigation behavior
history.pushState(null, '', el.getAttribute('href')) // Enter the browser's cache stack
renderView(location.pathname)
})
})
}
function renderView(historyPath) {
routes.forEach(item => {
if (item.path === historyPath) {
document.getElementById('root').innerHTML = item.component()
}
})
}
6. Summary
The essence of front-end routing is to switch page content based on browser URL changes without refreshing the page. It maintains a route mapping table to associate different paths with different components. When the URL changes, the front end listens for this change, finds the corresponding component, and renders it on the page.
There are two common implementation methods for front-end routing: hash mode and history mode. Hash mode implements route switching by listening for the hashchange event. It is characterized by simple implementation and good compatibility, but the URL contains a #. History mode modifies the URL using history.pushState and cooperates with popstate to listen for browser forward/backward navigation. The URL is cleaner, but the project requires server-side fallback configuration when going live; otherwise, refreshing the page may result in a 404 error.