Deep Dive into Vue Router 4: createWebHistory, Web History API, and Source Code Analysis
This article explains how Vue Router 4 leverages the HTML5 Web History API through createWebHistory, detailing pushState/replaceState mechanics, server fallback handling, TypeScript definitions, the four‑step creation process, listener implementation, and comparisons with hash and memory histories, all illustrated with real source code.
The article begins by introducing the series on Vue Router 4 source code and states that it will explore the part of the code dealing with the Web History API, which underlies the official history mode of Vue Router.
Purpose
Readers will learn how Vue Router uses the Web History API, and understand the implementation details of createWebHistory and createWebHashHistory .
Web History API Basics
The H5 History API provides two key functions, pushState() and replaceState() , which change the URL without a server round‑trip. The length property reflects the number of entries in the session history stack, and scrollRestoration controls automatic or manual scroll position restoration.
When pushState is called, the browser URL changes but the page content stays the same, and the history length increments.
Server Adaptation
Using pushState or replaceState can cause a 404 on page refresh because the server does not recognize the client‑side path. The solution is to add a fallback route that serves index.html for any unmatched URL.
createWebHistory TypeScript Definition
export declare function createWebHistory(base?: string): RouterHistory
/**
* Interface implemented by History implementations that can be passed to the router as Router.history
*/
export interface RouterHistory {
readonly base: string
readonly location: HistoryLocation
readonly state: HistoryState
push(to: HistoryLocation, data?: HistoryState): void
replace(to: HistoryLocation, data?: HistoryState): void
go(delta: number, triggerListeners?: boolean): void
listen(callback: NavigationCallback): () => void
createHref(location: HistoryLocation): string
destroy(): void
}The article notes that createRouter creates a Vue Router instance whose history object is built by createWebHistory , exposing methods like push , replace , and listeners.
Implementation Process (Four Steps)
Create the router history object with properties location , state , push , and replace .
Create router listeners that handle state changes and custom navigation callbacks.
Add a location proxy so that accessing routerHistory.location returns a normalized path.
Add a state proxy so that accessing routerHistory.state returns the current history state.
The core source for step 1 is:
export function createWebHistory(base?: string): RouterHistory {
base = normalizeBase(base)
// Step 1: create history object
const historyNavigation = useHistoryStateNavigation(base)
// Step 2: create listeners
const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
const routerHistory: RouterHistory = assign({
location: '',
base,
go,
createHref: createHref.bind(null, base),
}, historyNavigation, historyListeners)
// Step 3: location proxy
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
// Step 4: state proxy
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
return routerHistory
}push and replace implementations
Both methods ultimately call a shared changeLocation function that builds the final URL, decides between pushState and replaceState , and updates the history stack.
function changeLocation(to: HistoryLocation, state: StateEntry, replace: boolean): void {
const hashIndex = base.indexOf('#')
const url = hashIndex > -1
? (location.host && document.querySelector('base') ? base : base.slice(hashIndex)) + to
: createBaseLocation() + base + to
try {
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state
} catch (err) {
if (__DEV__) {
warn('Error with push/replace State', err)
} else {
console.error(err)
}
location[replace ? 'replace' : 'assign'](url)
}
}The push method records the current scroll position, inserts a temporary entry to preserve it, then performs the final navigation; replace directly updates the state and URL.
Router Listeners
Listeners react to popstate , beforeunload , and expose three hook functions:
pauseListeners – temporarily stop listening.
listen – register a navigation callback and receive a teardown function.
destroy – remove all listeners and clean up event handlers.
const popStateHandler: PopStateListener = ({ state }) => {
const to = createCurrentLocation(base, location)
const from = currentLocation.value
const fromState = historyState.value
let delta = 0
if (state) {
currentLocation.value = to
historyState.value = state
if (pauseState && pauseState === from) {
pauseState = null
return
}
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta ? (delta > 0 ? NavigationDirection.forward : NavigationDirection.back) : NavigationDirection.unknown,
})
})
}
function beforeUnloadListener() {
const { history } = window
if (!history.state) return
history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), '')
}Hash and Memory Histories
The article also briefly covers createWebHashHistory (which uses the URL hash) and createMemoryHistory (used for SSR). The memory history keeps a queue of locations and a position index, providing push , replace , go , and listener management without relying on the browser's History API.
export function createMemoryHistory(base: string = ''): RouterHistory {
let listeners: NavigationCallback[] = []
let queue: HistoryLocation[] = [START]
let position = 0
base = normalizeBase(base)
function setLocation(location: HistoryLocation) {
position++
if (position === queue.length) {
queue.push(location)
} else {
queue.splice(position)
queue.push(location)
}
}
function triggerListeners(to, from, { direction, delta }) {
const info = { direction, delta, type: NavigationType.pop }
listeners.forEach(cb => cb(to, from, info))
}
const routerHistory: RouterHistory = {
location: START,
state: {},
base,
createHref: createHref.bind(null, base),
replace(to) {
queue.splice(position--, 1)
setLocation(to)
},
push(to, data) {
setLocation(to)
},
listen(cb) {
listeners.push(cb)
return () => {
const i = listeners.indexOf(cb)
if (i > -1) listeners.splice(i, 1)
}
},
destroy() {
listeners = []
queue = [START]
position = 0
},
go(delta, shouldTrigger = true) {
const from = this.location
const direction = delta < 0 ? NavigationDirection.back : NavigationDirection.forward
position = Math.max(0, Math.min(position + delta, queue.length - 1))
if (shouldTrigger) triggerListeners(this.location, from, { direction, delta })
},
}
Object.defineProperty(routerHistory, 'location', { enumerable: true, get: () => queue[position] })
return routerHistory
}Conclusion
The article wraps up by summarizing the benefits of the Web History API—URL changes without server interaction, state passing between routes, and broad browser compatibility—showing how Vue Router encapsulates these capabilities in its history implementations.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.