In vue- Router, the mode parameter controls the implementation mode of the route. Today let us look at the vue-Router source code is how to implement routing
Route origin – Back-end route
The idea of routing first came from the back end. In the past, when you were developing pages with a template engine, you often saw addresses like this
www.vueRouter.com/login
The general process can be viewed as follows:
- Browser requests
- The server listens for a port request, such as (80, 443), and parses the URL path
- Returns information (HTML characters, JSON data, images…) based on the server configuration.
- Does the browser decide how to parse data based on the packet’s Content-Type
That is, routing is an interaction mode with the back-end server. It is one of the functions of routing to request different resources and pages through different paths
The front-end routing
With the business functions of front-end applications becoming more and more complex and users’ requirements for user experience becoming higher and higher, single page application (SPA) has become the mainstream form of front-end applications. One of the most notable features of large single-page applications is the use of a front-end routing system that updates the page view by changing the URL without re-requesting the page.
“Updating the view without rerequesting the page” is one of the core principles of front-end routing. At present, there are two main ways to realize this function in the browser environment:
- Hash mode: Uses # in the browser
- History mode: Leverage a new method in HTML5 called the History Interface
Hash pattern
Hash example:
www.vueRouter.com/login
Hash mode: The hash value is the anchor part of the URL (starting with the # sign). Changing the hash value does not cause the browser to send a request to the server. If the browser does not send a request, the interface will not be refreshed. In addition, every time the hash value changes, the hashchange event is triggered, which tells us what the hash value has changed. We can then listen on hashchange to update parts of the page:
Function updateDom () {window.addeventListener ('hashchange', updateDom) {window.addeventListener ('hashchange', updateDom)Copy the code
The history mode
If you don’t want ugly hashes, you can use the history mode of the route, which takes full advantage of the history.pushState API to do URL jumps without reloading the page.
const router = new VueRouter({
mode: 'history',
routes: [...]
})
Copy the code
Source code analysis
We find the definition of the VueRouter class and extract the parts related to the mode parameter as follows:
export default class VueRouter { mode: string; / / the incoming string parameters, indicating the history category history: HashHistory | HTML5History | AbstractHistory; Fallback: Boolean; fallback: Boolean; // If the browser does not support this, the 'history' mode should be rolled back to the 'hash' mode. Constructor (options: RouterOptions = {}, {let mode = options. The mode | | 'hash' / / the default for this. 'hash' mode fallback mode of = = = = 'history' &&! SupportsPushState check whether the browser supports 'history' if (this.fallback) {mode = 'hash'} if (! InBrowser) {mode = 'abstract'} this.mode = mode and instantiate 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) break default: if (process.env.NODE_ENV ! == 'production') { assert(false, `invalid mode: ${mode}`) } } } init (app: Any /* Vue component instance */) {const history = this.history // executes initialization based on the class of history and listens if (history instanceof) HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // The following methods exposed by the VueRouter class are actually methods that call the concrete history object 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
-
The string attribute mode, passed in as an argument, is just a marker to indicate the implementation class of the object property history that is actually in play
-
If the browser does not support HTML5History (as determined by the supportsPushState variable), mode is forced to be set to ‘hash’. If not running in a browser environment, mode is forced to ‘abstract’
-
VueRouter’s onReady(), push() and other methods are proxies that call the corresponding methods of the specific history object and perform different operations according to the specific class of the history object when initialized in init()
In the browser environment of the two ways, respectively is in HTML5History, HashHistory two classes to achieve. They are all defined in the SRC /history folder, inherits from the history class defined in the base.js file in the same directory. History is a definition of common and basic methods, so it’s confusing to look at them directly. Let’s start by looking at the friendly push() and replace() methods in HTML5History and HashHistory.
HashHistory.push()
First, 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:
window.location.hash = route.fullPath
Copy the code
Hash changes 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? : Function) { const route = this.router.match(location, 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
According to the notes, APP is an instance of Vue component, but we know that as a progressive front-end framework, Vue should not have the built-in attribute _route in its component definition. If there is such attribute in the component, it should be in the place where the plug-in is loaded. VueRouter’s install() method is mixed into the Vue object.
export function install (Vue) {
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
registerInstance(this, this)
},
})
}
Copy the code
The vue.mixin () method globally registers a mix that affects every Vue instance created after registration. The mix defines the reactive _route attribute 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.
In summary, the flow from setting route changes to view updates is as follows:
$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
Copy the code
HashHistory.replace()
The replace() method differs from the push() method in that instead of adding a new route to the top of the browser’s access history stack, it replaces the current route:
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('#') window.location.replace( window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path ) }Copy the code
Listening address bar
Vuerouter.push () and vuerouter.replace () discussed above can be called directly from the logical code of the Vue component. In addition, in the browser, the user can enter the change route directly into the browser address bar. So VueRouter also needs to be able to listen for routing changes in the browser address bar and have the same response behavior as calling from code. In HashHistory this is done via setupListeners:
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
HTML5History
History Interface is the interface provided by the browser History stack. By using methods such as back(), forward(), and Go (), we can read the browser History stack information and perform various jump operations.
window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)
Copy the code
Vue router router router router router router router
push (location: RawLocation, onComplete? : Function, onAbort? : Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete? : Function, onAbort? : Function) { const { current: fromRoute } = this this.transitionTo(location, route => { replaceState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } // src/util/push-state.js export function pushState (url? : string, replace? : boolean) { saveScrollPosition() // try... catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls const history = window.history try { if (replace) { history.replaceState({ key: _key }, '', url) } else { _key = genKey() history.pushState({ key: _key }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } export function replaceState (url? : string) { pushState(url, true) }Copy the code
Adding a listener to HTML5History to modify the browser’s address bar URL is done directly in the constructor:
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
HTML5History, of course, uses HTML5’s new feature, which requires browser version-specific support. This is checked using the supportsPushState variable:
// src/util/push-state.js export const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent if ( (ua.indexOf('Android 2.') ! = = 1 | | ua. IndexOf (" Android 4.0 "). == -1) && ua.indexOf('Mobile Safari') ! == -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1 ) { return false } return window.history && 'pushState' in window.history })()Copy the code
Compare the two modes
According to MDN, calling history.pushState() has the following major advantages over modifying hash directly:
-
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
AbstractHistory
Abstract mode is the easiest to handle because there is no association with browser address-related records; The overall flow is the same as for HashHistory, except that arrays are used to simulate the browser history stack.
export class AbstractHistory extends History { index: number; stack: Array<Route>; / /... push (location: RawLocation) {this.transitionto (location, route => {// Update historical stack information this.stack = this.stack.slice(0, This.index + 1).concat(route) this.index++})} replace (location: RawLocation) { this.transitionTo(location, Stack = this.stack. Slice (0, = this.stack. This.index).concat(route)})} Number) {/ / new history position const targetIndex = this. Index + n / / beyond returned if (targetIndex < 0 | | targetIndex > = This.stack.length) {return} // Get a new route object // Because it is browser neutral, we must get a const route = this.stack[targetIndex] // ConfirmTransition this. ConfirmTransition (route, () => {// update this.index = targetIndex this.updateroute (route)})} ensureURL () {// noop}}Copy the code
summary
The entire history-related code has been analyzed here. Although there are three modes, the overall execution process is the same, with the only difference being the specific logic used to handle location updates.
Welcome to brick
The resources
Vue-router Two types of front-end routing from vue-router Overview of front-end routing and vue-router implementation principles