I used Vue to develop a single page application and found that no matter how the route changes, the browser address bar always has a ‘#’ number.
At that time, I checked my code and did not find the requested address with ‘#’. I was also puzzled at that time, but I did not care about it at that time because it did not affect the rendering of the page and send requests to the background. A recent look at vue-Router implementation revealed the mystery.
Two modes of vue-router (in browser environment)
1. Hash (corresponding to HashHistory)
The hash (” # “) symbol is supposed to be added to a URL to indicate the location of the page:
http://www.example.com/index.html#print
Copy the code
The # symbol itself and the character following it are called hash(that’s why I had a ‘#’ in the address bar before) and can be read using the window.location.hash property. It has the following characteristics:
- The hash appears in the URL but is not included in the HTTP request. It is used to direct browser action and is completely useless on the server side, so changing the hash does not reload the page
2. You can add listening events for hash changes:
window.addEventListener("hashchange", funcRef, false)
Copy the code
- Each change to hash (window.location.hash) adds a record to the browser’s access history
Using the above features of hash, it is possible to implement a front-end route that “updates the view without rerequesting the page.”
2. History (HTML5History)
The History interface is provided by the browser History stack. Through methods like back(), forward(), and Go (), we can read the browser History stack information and perform various jump operations.
Starting with HTML5, the History Interface provides two new methods: pushState() and replaceState() that allow us to modify the browser History stack:
window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)
Copy the code
StateObject: When the browser jumps to a new state, the popState event fires, which carries a copy of the stateObject parameter title: The title URL of the record added: the URL of the record added
The two methods have a common feature: when they are called to modify the browser history stack, the browser does not refresh the page even though the current URL has changed, which provides the basis for single-page application front-end routing “update the view without rerequesting the page.” Browser history can be thought of as a “stack.” A stack is a last-in, first-out structure, which can be thought of as a stack of plates. Each time a user clicks on a new web page, a new plate is added to it, called “pushing”. Each time the user clicks the Back button, the top plate is removed, called “out of the stack.” Each time the browser displays the contents of the top plate.
Vue – the function of the router
Vue-router is used to update the page view without re-requesting the page by changing the URL. Simply put, although the address bar has been changed, it is not a new page, but some parts of the previous page have been changed.
export default new Router({
// mode: 'history'// routes: constantRouterMap})Copy the code
This is a common code for initializing vue-Router in Vue projects. Before I studied vue-Router carefully, I did not know that there is a mode attribute. Later I read related articles and learned that the mode attribute is used to specify which mode the vue-Router uses. If no mode value is specified, hash mode is used.
Source code analysis
First, take a look at the vue-Router constructor
constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this)let mode = options.mode || 'hash'
this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract'This. history = new AbstractHistory(this, options.base) : // AbstractHistory = new AbstractHistory(this, options.base)break
default:
if(process.env.NODE_ENV ! = ='production') {
assert(false, `invalid mode: ${mode}`)}}}Copy the code
Obtain the mode value first. If the mode value is history but the browser does not support the history mode, force the mode value to hash. History if supported. Next, based on the value of mode, choose which mode vue-Router will use.
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
Copy the code
So there are two patterns. Once you’ve determined which mode vue-Router uses, it’s time to init. Let’s take a look at what the Router init method does, in SRC /index.js
init (app: any /* Vue component instance */) {
// ....
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (historyinstanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // .... // The VueRouter class exposes the following methods that are actually call-specifichistoryObject method push (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.history.push(location, onComplete, onAbort) } replace (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.history.replace(location, onComplete, onAbort) } }Copy the code
If it is HTML5History, execute
history.transitionTo(history.getCurrentLocation())
Copy the code
If the Hash mode is used, the command is executed
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
Copy the code
As you can see, both modes execute the transitionTo() function. Let’s take a look at how the two modes are executed. First, let’s look at the Hash mode. Okay
HashHistory.push()
Let’s look at the push() method in HashHistory:
push (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.transitionTo(location, route => { pushHash(route.fullPath) onComplete && onComplete(route) }, onAbort) }function pushHash (path) {
window.location.hash = path
}
Copy the code
The transitionTo() method is defined in the parent class to handle the underlying logic of route changes, while the push() method performs a direct hash assignment to the window:
Changes to window.location.hash = route.fullPath hash are automatically added to the browser’s access history.
To update a view, look at the transitionTo() method in the parent class History:
transitionTo (location: RawLocation, onComplete? : Function, onAbort? : Const route = this.router. Match (location,) {Function) {const route = this.router. this.current) this.confirmTransition(route, () => { this.updateRoute(route) ... }) } updateRoute (route: Route) { this.cb && this.cb(route) } listen (cb: Function) { this.cb = cb }Copy the code
As you can see, when the route changes, the this.cb method in History is called, and this. Cb is set by history.listen (cb). Going back to the VueRouter class definition, I found it set in the init() method:
init (app: any /* Vue component instance */) {
this.apps.push(app)
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
Copy the code
The app in the code refers to the instance of Vue. The._route is not a built-in attribute defined in the component itself, but is globally registered as a hybrid with vue.mixin () when vue.use (Router) loads vue-Router. Affecting every Vue instance created after registration, the blend defines a reactive _route in the beforeCreate hook via vue.util.definereActive (). When the _route value changes, the render() method of the Vue instance is automatically called to update the view. Vm. render() is used to render the component corresponding to the route according to the path, name and other attributes of the current _route. So in summary, the update flow from route change to view is as follows:
this.$router.push(path) --> HashHistory.push() --> History.transitionTo() --> const route = this.router.match(location, This.current) will do the address matching, UpdateRoute (route) --> app._route=route (Vue instance _route changed) --> app._route=route (Vue instance _route changed) When the value of _route changes, Render () --> vm.render() in <router-view></router-view> render --> window.location.hash = route.fullpath (The browser address bar shows the path of the new route)Copy the code
HashHistory.replace()
Having said hashhistory.push (), it’s time to say hashhistory.replace ().
replace (location: RawLocation, onComplete? : Function, onAbort? : Function) { this.transitionTo(location, route => { replaceHash(route.fullPath) onComplete && onComplete(route) }, onAbort) }function replaceHash (path) {
const i = window.location.href.indexOf(The '#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + The '#' + path
)
}
Copy the code
As you can see, hashhistory.replace is structurally similar to push(), except that instead of assigning a value directly to window.location.hash, it calls the window.location.replace method to replace the route. This does not add the new route to the top of the browser’s access history stack, but replaces the current route.
Listening address bar
As you can see, the above procedures are all done inside the code, such as this.$router.push(), which is common in projects. Then set the browser’s address bar to the new hash value. What if you changed the route by typing the URL directly into the address bar, for example
setupListeners () {
window.addEventListener('hashchange', () = > {if(! ensureSlash()) {return
}
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
})
}
Copy the code
This method setting listens for the browser event hashchange and calls the replaceHash function, meaning that entering the route directly into the browser address bar is equivalent to code calling the replace() method. The next steps are of course the same as hashHistory.replace () for page rendering.
HTML5History
The vue-Router code structure of HTML5History mode and the logic of updating the view are basically similar to hash mode and the steps of HashHistory mode. Only HashHistory push and replace () into HTML5History. PushState () and HTML5History replaceState ()
Adding a listener to HTML5History to modify the browser’s address bar URL is done directly in the constructor, listening for the popState event in HTML5History:
constructor (router: Router, base: ? string) { window.addEventListener('popstate', e => {
const current = this.current
this.transitionTo(getLocation(this.base), route => {
if (expectScroll) {
handleScroll(router, route, current, true)}})})}Copy the code
This is the analysis of the processing logic in the different modes of vue-Router hash mode and History mode.
Compare the two modes
The hash mode adds a ‘#’ to the browser URL. HTM5History does not have a ‘#’. The URL is the same as a normal URL. History.pushstate () has the following advantages over directly modifying hash:
- PushState sets the new URL to any URL of the same origin as the current URL. Hash can only change the part after #, so you can only set the URL of the current document
- PushState sets the new URL to be exactly the same as the current URL, which also adds the record to the stack. The new hash value must be different to trigger the record to be added to the stack
- PushState stateObject allows you to add any type of data to a record. Hash can only add short strings
- PushState sets the title property in addition for later use
Vue-router source code analysis – the overall process using the History API without refreshing change address bar