preface
When we use vUe-related ecology, we can’t avoid using Vue Router. However, we don’t know much about how Vue Router helps us manage routes, render pages and jump paths. This paper mainly analyzes its principle from some scenes used by most students.
- Vue Router: Import Router and new Router(
- Vue Router Jump analysis (Part) : Route matcher logic analysis
- Vue Router Jump analysis (Part 2) : Use the route matcher to find the route and jump
1. Start to write a Router
Let’s take the Vue single page application as an example:
main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
import App from './App';
import router from './router';
Vue.config.productionTip = false;
router.beforeEach((to, from, next) = > {
console.log('global router beforeEach');
next();
});
router.beforeResolve((to, from, next) = > {
console.log('global router beforeResolve');
next();
});
router.afterEach((to, from) = > {
console.log('global router afterEach ====='.from.name);
});
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
template: '<App />'.components: {
App,
},
});
Copy the code
router/index.js
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
const Foo = {
template: `
Foo
to-child
`,
beforeRouteEnter(to, from, next) {
console.log('Foo beforeRouteEnter');
next((vm) = > {
console.log(vm);
});
},
beforeRouteUpdate(to, from, next) {
console.log('Foo beforeRouteUpdate');
next();
},
beforeRouteLeave(to, from, next) {
console.log('Foo beforeRouteLeave'); next(); }};const Child = { template: '<div>Foo Child</div>' };
const Bar = { template: '<div>Bar</div>' };
export default new Router({
routes: [{path: '/foo'.name: 'Foo'.component: Foo,
meta: { permission: true },
children: [{path: 'child/:id'.name: 'Child'.component: Child,
},
],
},
{
path: '/bar'.name: 'Bar'.component: Bar,
meta: { permission: true },
beforeEnter: (to, from, next) = > {
console.log('Bar router beforeEnter'); next(); }},]});Copy the code
App.vue
<template>
<div id="app">
<h1>Hello Router</h1>
<div>
<router-link to="/foo">to foo</router-link>
<router-link to="/bar">to bar</router-link>
</div>
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
<style lang="css">
#app {
text-align: center;
margin-top: 200px;
}
#app > div {
margin-bottom: 20px;
}
#app > div > a {
margin: 10px;
}
</style>
Copy the code
Run the project as follows:
Let’s start with a step-by-step analysis of the Vue Router.
2. import Router from ‘vue-router’
This step introduces the VueRouter class from SRC /index.js, with an install static method and version version number. Class defines constructors and methods.
3. Vue.use(Router)
Here are two objects introduced in advance:
- Router: VueRouter A routing object that provides a series of methods to manipulate routes for example
this.$router.push
And so on; - Route: Path object that provides some parameters of the path, for example
this.$route.query
And so on;
The vue.use () method gives the plug-in a method injected into the Vue that calls the plug-in’s install method, which is in SRC /install.js:
import View from './components/view'
import Link from './components/link'
export let _Vue // Export Vue objects for use elsewhere
export function install (Vue) {
// both the instanlLED method and the _Vue object
// If install is already installed, avoid repeated installation
if (install.installed && _Vue === Vue) return
install.installed = true // Installed flag is set to the installed state
_Vue = Vue // Store Vue objects
const isDef = v= >v ! = =undefined // check whether the variable is defined as a function
// Register instance method: register the registerRouteInstance method in the data of the parent node, which will be referred to later // todo
// If registerInstance callVal is not empty, register the Route. If registerInstance callVal is not empty, register the Route.
const registerInstance = (vm, callVal) = > {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// Blend into Vue
Vue.mixin({
beforeCreate () {
// This is the router we passed in main.js
if (isDef(this.$options.router)) {
this._routerRoot = this // Root Vue instance
this._router = this.$options.router // The current router object
this._router.init(this) // Initialize the router
// definedReact sets this._route to a responsive object
Vue.util.defineReactive(this.'_route'.this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this // Take the root Vue instance
}
registerInstance(this.this) // Register the Route instance
},
destroyed () {
registerInstance(this) // Destroy the Route instance}})// Mount the router instance to $router, so we can access the router object in vue file through this.$router
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// Mount the route instance to $route, as above
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// The registered RouterView and RouterLink components pass first.
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// Mount beforeRouteEnter, beforeRouteLeave, beforeRouteUpdate methods to Vue
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
Copy the code
Following the this._router.init(this) logic above, init is defined in SRC /index.js and is an instance of VueRouter.
init (app: any /* Vue component instance */) { process.env.NODE_ENV ! = ='production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app)
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () = > {// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > - 1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] | |null
})
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = (a)= > {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route= > {
this.apps.forEach((app) = > {
app._route = route
})
})
}
Copy the code
After determining whether to install the plug-in, the VM is stored in the Apps array. When the VM is destroyed, the VM is also removed from the apps to avoid memory overflow.
Then check if this.app exists, initialize it if not, and get the history object, which is introduced below when new Router() initializes the Router object. Since we often use the hashHistory pattern, we’ll start with this section.
Else if defines a setupHashListener function, which registers the history event (scroll behavior and jump operation when route changes);
Then call the transitionTo method to jump a route, passing in currentLocation and the setupListeners function;
Finally, the history.listen method is called to mount the anonymous function to the cb property of history.
4. Initialize the Router object
After we have written vue.use (), it is time to initialize the Router object:
const router = new Router({
routes: [{path: '/'.component: HelloWorld
},
],
});
Copy the code
This parameter object also supports a few other properties: mode – hash mode by default, scrollBehavior – scrolling effect when switching routes, Base – base path, and more. See the build options on the official website.
The Router class is defined under SRC /index.js.
constructor (options: RouterOptions = {}) {
this.app = null // The current VM instance
this.apps = [] / / vm array
this.options = options / / configuration items
this.beforeHooks = [] // Array of navigational guard functions before the hook function is executed
this.resolveHooks = [] // Array of navigational guard functions when the hook function executes
this.afterHooks = [] // Navigation hook function array after hook function execution
this.matcher = createMatcher(options.routes || [], this) // * Path matcher, described later
let mode = options.mode || 'hash' // The default is hash
// If the history mode is not supported, revert to hash mode
this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
if (this.fallback) {
mode = 'hash'
}
if(! inBrowser) { mode ='abstract'
}
this.mode = mode
// Initialize the history object
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}`)}}}Copy the code
Let’s look at the HashHistory object, SRC /history/hash.js (constructor).
export class HashHistory extends History {
constructor(router: Router, base: ? string, fallback: boolean) {// Call the parent constructor
super(router, base)
//
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
}
Copy the code
Since a fallback is not normally defined, the ensureSlash function is entered:
function ensureSlash () :boolean {
const path = getHash()
if (path.charAt(0) = = ='/') {
return true
}
replaceHash('/' + path)
return false
}
export function getHash () :string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
let href = window.location.href
const index = href.indexOf(The '#')
// empty path
if (index < 0) return ' '
href = href.slice(index + 1)
// decode the hash but not the search or hash
// as search(query) is already decoded
// https://github.com/vuejs/vue-router/issues/2708
const searchIndex = href.indexOf('? ')
if (searchIndex < 0) {
const hashIndex = href.indexOf(The '#')
if (hashIndex > - 1) {
href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex)
} else href = decodeURI(href)
} else {
if (searchIndex > - 1) {
href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex)
}
}
return href
}
Copy the code
Super, executes the superclass constructor. The parent class constructor is defined in SRC /history/base.js.
constructor(router: Router, base: ? string) {this.router = router
this.base = normalizeBase(base) // this.base = ""
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
function normalizeBase (base: ? string) :string {
if(! base) {if (inBrowser) {
// respect <base> tag
const baseEl = document.querySelector('base') // baseEl = null
base = (baseEl && baseEl.getAttribute('href')) || '/' // base = "/"
// strip full URL origin
base = base.replace(/^https? : \ \ [^ \] / / / + /.' ') // base = "/"
} else {
base = '/'}}// make sure there's the starting slash
if (base.charAt(0)! = ='/') {
base = '/' + base
}
// remove trailing slash
return base.replace(/ / / $/.' ') // return ""
}
// src/util/route.js
// An empty Route is created as the starting path. More on createRoute later.
export const START = createRoute(null, {
path: '/'
})
Copy the code
There is a base and an empty path, and the debugger result is shown as follows:
If (index < 0) will return an empty string. EnsureSlash will return path as an empty string. The replaceHash argument is ‘/’ :
function replaceHash (path) { // path = "/"
if (supportsPushState) { // supportsPushState = true
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
// src/util/push-state.js
export const supportsPushState =
inBrowser &&
(function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.')! = =- 1 || ua.indexOf('the 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
})()
// src/util/push-state.js
function getUrl (path) { // path = "/"
const href = window.location.href // href = "http://localhost:8080/"
const i = href.indexOf(The '#') // i = -1
const base = i >= 0 ? href.slice(0, i) : href // base = "http://localhost:8080/"
return `${base}#${path}` // return "http://localhost:8080/#/"
}
// src/util/push-state.js
export function replaceState (url? : string) { // url = "http://localhost:8080/#/"
pushState(url, true)}// src/util/push-state.js
// url = "http://localhost:8080/#/", replace = true
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) {
/ / call window. History. ReplaceState change the url, the key value is through the performance. Now take to the (), more precise
history.replaceState({ key: getStateKey() }, ' ', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, ' ', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
Copy the code
PushState = localhost:8080/#/
conclusion
Here we have a brief understanding of the introduction and initialization of the Vue Router. $router and $route are bound to Vue and http://localhost:8080 is changed to http://localhost:8080/#/. That’s the Matcher object we talked about earlier.