By Dolymood, Didi public Front End team
In the era of single-page applications, routing has become an essential tool for developing applications; Throughout the framework, there will be a corresponding strong routing support. Vue.js has become the new favorite of the majority of front-end users because of its performance, general, easy to use, volume, low learning cost, and its corresponding routing vue-Router is also designed simple and easy to use, powerful function. This article from the source code to analyze the vue. js official route vue-Router overall process.
This paper mainly analyzes vue-Router version 2.0.3.
Let’s start with the big picture:
To get a general impression of the whole process, let’s take examples/basic examples from the official warehouse to analyze the whole process.
The directory structure
Let’s look at the overall directory structure:
components
history
create-matcher.js
create-route-map.js
index.js
install.js
The entrance
Let’s start with the code section of the application entry:
import Vue from 'vue'
import VueRouter from 'vue-router'
/ / 1. Plug-in
// Install
and
components
// Inject $router and $route objects into all components of the current application
Vue.use(VueRouter)
// 2. Define the components used in each route
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
Create VueRouter instance router
const router = new VueRouter({
mode: 'history'.base: __dirname,
routes: [{path: '/'.component: Home },
{ path: '/foo'.component: Foo },
{ path: '/bar'.component: Bar }
]
})
// 4. Create a startup application
// Be sure to inject the router
// The routing component will be rendered in
new Vue({
router,
template: `
Basic
-
/
-
/foo
-
/bar
/bar
`
}).$mount('#app')Copy the code
As a plug-in
The crucial first step in the code above is to install VueRouter using the plugins mechanism.use(plugin) provided by vuue.js. The plugin mechanism calls the install method of the plugin object (of course, the plugin itself is called as a function if the plugin doesn’t have the method). Let’s take a look at the implementation of vue-Router.
The VueRouter object, which is exposed in SRC /index.js, has a static install method:
/* @flow */
// Import the install module
import { install } from './install'
// ...
import { inBrowser, supportsHistory } from './util/dom'
// ...
export default class VueRouter {
// ...
}
Install / / assignment
VueRouter.install = install
// Automatically use plug-ins
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}Copy the code
As you can see, this is a classic way to write a vue. js plug-in. Add install to the plugin object to install the plugin logic, and at the end of the day, the plugin will be automatically used if it is in a browser environment and window.Vue exists.
Install is a separate module here, continuing with the main logic of SRC /install.js at the same level:
// router-view Router-link component
import View from './components/view'
import Link from './components/link'
// export a Vue reference
export let _Vue
// Install the function
export function install (Vue) {
if (install.installed) return
install.installed = true
// Assign a private Vue reference
_Vue = Vue
$router $route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this.$root._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this.$root._route }
})
// beforeCreate mixin
Vue.mixin({
beforeCreate () {
// Check whether there is a router
if (this.$options.router) {
/ / assignment _router
this._router = this.$options.router
// Initialize init
this._router.init(this)
// Define a responsive _route object
Vue.util.defineReactive(this.'_route'.this._router.history.current)
}
}
})
// Register the component
Vue.component('router-view', View)
Vue.component('router-link', Link)
// ...
}Copy the code
There’s a little bit of a question here, right?
- Why export a Vue reference?
The plugin does not want to package the vue as a dependency package, but if you want to use some of the methods of the vue object itself, you can use a similar approach. This allows you to use some of Vue’s methods elsewhere without importing Vue dependencies (as long as you’re sure to use them after install).
- By giving
Vue.prototype
define$router
,$route
Properties to inject them into all components?
All components in vue.js are extended Vue instances, which means that all components have access to properties defined on the instance prototype.
BeforeCreate mixin will be discussed later when creating Vue instances.
instantiationVueRouter
In the entry file, you instantiate a VueRouter and pass it into the Options of the Vue instance. Now look at the VueRouter class exposed in SRC /index.js:
// ...
import { createMatcher } from './create-matcher'
// ...
export default class VueRouter {
// ...
constructor (options: RouterOptions = {}) {
this.app = null
this.options = options
this.beforeHooks = []
this.afterHooks = []
// Create a match function
this.match = createMatcher(options.routes || [])
// Instantiate the specific History according to mode
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsHistory
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)
break
default:
assert(false.`invalid mode: ${mode}`)}}// ...
}Copy the code
This includes an important step: creating the match function.
match
Matching function
The matching function is created by createMatcher in SRC /create-matcher.js:
/* @flow */
import Regexp from 'path-to-regexp'
// ...
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher (routes: Array<RouteConfig>) :Matcher {
// Create a route map
const { pathMap, nameMap } = createRouteMap(routes)
// Match function
function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {
// ...
}
function redirect (record: RouteRecord, location: Location) :Route {
// ...
}
function alias (record: RouteRecord, location: Location, matchAs: string) :Route {
// ...
}
function _createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom)
}
/ / return
return match
}
// ...Copy the code
The specific logic will be analyzed later. Now we only need to understand that the corresponding route map is generated based on the incoming routes configuration, and then the match matching function is directly returned.
SRC /create-route-map.js createRouteMap:
/* @flow */
import { assert, warn } from './util/warn'
import { cleanPath } from './util/path'
// Create a route map
export function createRouteMap (routes: Array<RouteConfig>) :{
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// path Route map
const pathMap: Dictionary<RouteRecord> = Object.create(null)
// name Route map
const nameMap: Dictionary<RouteRecord> = Object.create(null)
// Traverse the route configuration object to add a route record
routes.forEach(route= > {
addRouteRecord(pathMap, nameMap, route)
})
return {
pathMap,
nameMap
}
}
// Add a route record function
function addRouteRecord (pathMap: Dictionary
, nameMap: Dictionary
, route: RouteConfig, parent? : RouteRecord, matchAs? : string
) {
// Get path and name
const{ path, name } = route assert(path ! =null.`"path" is required in a route configuration.`)
// Route record object
const record: RouteRecord = {
path: normalizePath(path, parent),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {}
}
// Nested subroutes add records recursively
if (route.children) {
// ...
route.children.forEach(child= > {
addRouteRecord(pathMap, nameMap, child, record)
})
}
// Process alias logic to add the corresponding record
if(route.alias ! = =undefined) {
if (Array.isArray(route.alias)) {
route.alias.forEach(alias= > {
addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path)
})
} else {
addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path)
}
}
// Update the path map
pathMap[record.path] = record
// Update the name map
if (name) {
if(! nameMap[name]) { nameMap[name] = record }else {
warn(false.`Duplicate named routes definition: { name: "${name}", path: "${record.path}" }`)}}}function normalizePath (path: string, parent? : RouteRecord) :string {
path = path.replace(/ / / $/.' ')
if (path[0= = ='/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)}Copy the code
It can be seen that the main thing to be done is to generate a map of common routing records corresponding to path and routing records corresponding to name according to the user route configuration object, so as to facilitate subsequent matching and correspondence.
Instantiation of the History
This is also an important step. All History classes are in the SRC/History/directory. We don’t need to worry about the implementation differences for each History class right now. Just know that they all inherit from the history class in SRC /history/base.js:
/* @flow */
// ...
import { inBrowser } from '.. /util/dom'
import { runQueue } from '.. /util/async'
import { START, isSameRoute } from '.. /util/route'
// Export _Vue from install.js, which was analyzed earlier
import { _Vue } from '.. /install'
export class History {
// ...
constructor(router: VueRouter, base: ? string) {this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
}
// ...
}
// Get the base value
function normalizeBase (base: ? string) :string {
if(! base) {if (inBrowser) {
// respect <base> tag
const baseEl = document.querySelector('base')
base = baseEl ? baseEl.getAttribute('href') : '/'
} else {
base = '/'}}// make sure there's the starting slash
if (base.charAt(0)! = ='/') {
base = '/' + base
}
// remove trailing slash
return base.replace(/ / / $/.' ')}// ...Copy the code
Having instantiated the VueRouter, it’s time to look at the Vue instance.
instantiationVue
Instantiation is simple:
new Vue({
router,
template: `
Basic
-
/
-
/foo
-
/bar
/bar
`
}).$mount('#app')Copy the code
The router and template are passed in options. Create a new Vue instance and the beforeCreate hook will be called for each new Vue instance.
// ...
Vue.mixin({
beforeCreate () {
// Check whether there is a router
if (this.$options.router) {
/ / assignment _router
this._router = this.$options.router
// Initialize init
this._router.init(this)
// Define a responsive _route object
Vue.util.defineReactive(this.'_route'.this._router.history.current)
}
}
})Copy the code
To be specific, check whether the router is included in the options during the instantiation. If the router is included, it means that an instance with routing configuration is created. In this case, it is necessary to continue initializing the route-related logic. We then assign _router to the current instance so that when we access $router on the prototype, we get the router.
Let’s look at the two keys: router.init and defining a responsive _route object.
router.init
Now let’s see what the router init method does, again in SRC /index.js:
/* @flow */
import { install } from './install'
import { createMatcher } from './create-matcher'
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
import { inBrowser, supportsHistory } from './util/dom'
import { assert } from './util/warn'
export default class VueRouter {
// ...
init (app: any /* Vue component instance */) {
// ...
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(getLocation(history.base))
} else if (history instanceof HashHistory) {
history.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
history.onHashChange()
})
})
}
history.listen(route= > {
this.app._route = route
})
}
// ...
}
// ...Copy the code
It can be seen that initialization is mainly to assign values to app, which is special for HTML5History and HashHistory, because only in these two modes can there be a default page when entering, and the corresponding route needs to be activated according to the path or hash in the address bar of the current browser. This is done by calling transitionTo; There is also a special handling for HashHistory. Why not just listen for the HashChange event when initializing HashHistory? This is to fix github.com/vuejs/vue-r… BeforeEnter hook is fired twice if it is asynchronous in a hook function like beforeEnter. The reason for this is that the beforeEnter hook is initialized with #/ if the hash value does not start with a /. This process triggers the Hashchange event, so the lifecycle hook is walked again, which means the beforeEnter hook function is called again.
To take a look at the general logic of the specific transitionTo method, SRC /history/base.js:
/* @flow */
import type VueRouter from '.. /index'
import { warn } from '.. /util/warn'
import { inBrowser } from '.. /util/dom'
import { runQueue } from '.. /util/async'
import { START, isSameRoute } from '.. /util/route'
import { _Vue } from '.. /install'
export class History {
// ...transitionTo (location: RawLocation, cb? :Function) {
// Call match to get the matching route object
const route = this.router.match(location, this.current)
// Confirm the transition
this.confirmTransition(route, () => {
// Update the current route object
this.updateRoute(route)
cb && cb(route)
// Update the url of the subclass implementation
// Update the hash value for hash mode
// PushState/replacEstate is used for history mode
// Browser address
this.ensureURL()
})
}
// Confirm the transition
confirmTransition (route: Route, cb: Function) {
const current = this.current
// If it is the same, return directly
if (isSameRoute(route, current)) {
this.ensureURL()
return
}
// Cross-compares the routing record of the current route with that of the current route
// In order to be able to accurately get parent-child routing updates can be exactly known
// Which components need to be updated and which do not
const {
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
// Queue for the entire switchover cycle
const queue: Array<? NavigationGuard> = [].concat(// Leave's hook
extractLeaveGuards(deactivated),
// Global router before hooks
this.router.beforeHooks,
// The beforeEnter hook for the route to be updated
activated.map(m= > m.beforeEnter),
// Asynchronous components
resolveAsyncComponents(activated)
)
this.pending = route Specifies the iterator function executed by each queueconst iterator = (hook: NavigationGuard, next) = > {
// Ensure that the period is still the current route
if (this.pending ! == route)return
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)}else if (typeof to === 'string' || typeof to === 'object') {
// next('/') or next({ path: '/' }) -> redirect
this.push(to)
} else {
// confirm transition and pass on the value
next(to)
}
})
}
// Execute queue
runQueue(queue, iterator, () => {
const postEnterCbs = []
// The hook inside the component
const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
return this.current === route
})
// Execute the hook in the component after the last queue execution
// Because you need to wait for asynchronous components and OK to execute
runQueue(enterGuards, iterator, () => {
// Ensure that the period is still the current route
if (this.pending === route) {
this.pending = null
cb(route)
this.router.app.$nextTick((a)= > {
postEnterCbs.forEach(cb= > cb())
})
}
})
})
}
// Update the current route object
updateRoute (route: Route) {
const prev = this.current
this.current = route
// Note the value of cb
// Every update will be called with the need below!
this.cb && this.cb(route)
// Perform the After hooks callback
this.router.afterHooks.forEach(hook= > {
hook && hook(route, prev)
})
}
}
// ...Copy the code
As you can see, the whole process is about implementing the various hooks of the convention and dealing with asynchronous components. Some of the function-specific details have been left out (more on that later), but it doesn’t affect understanding the process. However, we need to pay attention to a concept: route record. Every route object should have a matched attribute, which corresponds to the route record. Its specific meaning will be dealt with in the call to match(). SRC /create-matcher.js: SRC /create-matcher.js
// ...
import { createRoute } from './util/route'
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher (routes: Array<RouteConfig>) :Matcher {
const { pathMap, nameMap } = createRouteMap(routes)
// 关键的 match
function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {
const location = normalizeLocation(raw, currentRoute)
const { name } = location
// Named route processing
if (name) {
// nameMap[name] = Route record
const record = nameMap[name]
const paramNames = getParams(record.path)
// ...
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
/ / create the route
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
// Common routing processing
location.params = {}
for (const path in pathMap) {
if (matchRoute(path, location.params, location.path)) {
// The route was successfully created
// pathMap[path] = Route record
return _createRoute(pathMap[path], location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
// ...
// Create a route
function _createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
// Redirection and alias logic
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
// Create a routing object
return createRoute(record, location, redirectedFrom)
}
return match
}
// ...Copy the code
SRC /util/route.js: SRC /util/route.js: SRC /util/route.js: SRC /util/route.js: SRC /util/route.js
// ...
export function createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
// You can see that it is a normal object that is frozen
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/'.hash: location.hash || ' '.query: location.query || {},
params: location.params || {},
fullPath: getFullPath(location),
// Get all matched routing records according to the record hierarchy
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom)
}
return Object.freeze(route)
}
// ...
function formatMatch (record: ? RouteRecord) :Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
// ...Copy the code
Back to init, the history.listen method is called:
history.listen(route= > {
this.app._route = route
})Copy the code
Listen simply sets the cb value of the current history object. This cb value is called when the history is updated. This function updates the _route value of the current application instance. Look at the analysis in the next paragraph.
DefineReactive definition _route
Return to the beforeCreate hook function, and finally define a response_route attribute for the current application instance using the tool method of Vue. The value is this._router.history.current. This is the currently active routing object for the current History instance. Defining such a responsive property value for an application instance means that if the property value changes, the update mechanism will be triggered and the application instance’s render will be called to re-render. The value of _route of the application instance will be updated every time history is successfully updated, which means that once history is changed, the update mechanism will trigger the application instance’s render method to re-render.
Router-link and router-view components
Back to instantiating the application instance:
new Vue({
router,
template: `
Basic
-
/
-
/foo
-
/bar
/bar
`
}).$mount('#app')Copy the code
You can see that the template of this instance contains two custom components: router-link and router-View.
The router – the view components
The router-view component is relatively simple, so we will analyze it first. It is defined in the source code SRC /components/view.js:
export default {
name: 'router-view'.functional: true.// Function components pure render
props: {
name: {
type: String.default: 'default' // Default default Specifies the name of the default named view
}
},
render (h, { props, children, parent, data }) {
// Solve the nesting depth problem
data.routerView = true
/ / the route objects
const route = parent.$route
/ / cache
const cache = parent._routerViewCache || (parent._routerViewCache = {})
let depth = 0
let inactive = false
// Depth of the current component
while (parent) {
if(parent.$vnode && parent.$vnode.data.routerview) {depth++} handles keepalive logicif (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// Get the routing record matching the current component level
const matched = route.matched[depth]
if(! matched) {return h()
}
// Get the component to render
const name = props.name
const component = inactive
? cache[name]
: (cache[name] = matched.components[name])
if(! inactive) {// The hook needs to be set every time in non-keepalive mode
// Then update (assign & destroy) matched instance elements
const hooks = data.hook || (data.hook = {})
hooks.init = vnode= > {
matched.instances[name] = vnode.child
}
hooks.prepatch = (oldVnode, vnode) = > {
matched.instances[name] = vnode.child
}
hooks.destroy = vnode= > {
if (matched.instances[name] === vnode.child) {
matched.instances[name] = undefined}}}// Call createElement to render the matching component
return h(component, data, children)
}
}Copy the code
As you can see the logic is relatively simple, just get the matching component and render it.
The router – link component
Take a look at the navigation link component, which is defined in the source SRC /components/link.js:
// ...
import { createRoute, isSameRoute, isIncludedRoute } from '.. /util/route'
// ...
export default {
name: 'router-link'.props: {
// The component properties passed in
to: { // The link to the destination route
type: toTypes,
required: true
},
// Create the HTML tag
tag: {
type: String.default: 'a'
},
// Full mode, if true then means
// Routes that are absolutely equal will add activeClass
// Otherwise it is containment
exact: Boolean.// Append the path to the current (relative) path
append: Boolean.// If true, call router.replace() to replace history
replace: Boolean.// The name of the CSS class used when the link is activated
activeClass: String
},
render (h: Function) {
// Get the router instance and the currently active Route object
const router = this.$router
const current = this.$route
const to = normalizeLocation(this.to, current, this.append)
// Based on the matching result of the current destination link and the currently active route
const resolved = router.match(to, current)
const fullPath = resolved.redirectedFrom || resolved.fullPath
const base = router.history.base
// Create the href
const href = createHref(base, fullPath, router.mode)
const classes = {}
// Activating class takes precedence over getting the linkActiveClass on the current component or router configuration
/ / the default router - link - active
const activeClass = this.activeClass || router.options.linkActiveClass || 'router-link-active'
// Compare the target
// There may not be a path because there are named routes
const compareTarget = to.path ? createRoute(null, to) : resolved
// Check if the route is the same if the mode is strict (path query params hash)
// Otherwise include logic (path contains, query contains hash is null or the same)
classes[activeClass] = this.exact
? isSameRoute(current, compareTarget)
: isIncludedRoute(current, compareTarget)
// Event binding
const on = {
click: (e) = > {
// Ignore clicks with function keys
if (e.metaKey || e.ctrlKey || e.shiftKey) return
// Blocked return
if (e.defaultPrevented) return
/ / right click
if(e.button ! = =0) return
/ / ` target = "_blank" ` ignored
const target = e.target.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
// Prevent a jump by blocking the default behavior
e.preventDefault()
if (this.replace) {
/ / replace logic
router.replace(to)
} else {
/ / push logic
router.push(to)
}
}
}
// Additional data is required to create the element
const data: any = {
class: classes
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
// Find the first to give this element the event binding and href attribute
const a = findAnchor(this.$slots.default)
if (a) {
// in case the <a> is a static node
a.isStatic = false
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
// If there is no , the current element is bound to itself
data.on = on
}
}
// Create the element
return h(this.tag, data, this.$slots.default)
}
}
function findAnchor (children) {
if (children) {
let child
for (let i = 0; i < children.length; i++) {
child = children[i]
if (child.tag === 'a') {
return child
}
if (child.children && (child = findAnchor(child.children))) {
return child
}
}
}
}
function createHref (base, fullPath, mode) {
var path = mode === 'hash' ? '/ #' + fullPath : fullPath
return base ? cleanPath(base + path) : path
}Copy the code
The router-link component calls router push or replace to update the route according to the value set to when it is clicked. It checks whether it matches the current route (strict match and contain match) to determine whether its activeClass is added.
summary
The code of the whole process has been analyzed almost here, let’s review:
I believe that the overall feeling is different from the initial feeling after seeing this picture, and I have a clear understanding of the overall process of VUe-Router. Of course, due to the limited space, there are still many details that have not been carefully analyzed, and specific analysis will be carried out according to modules in the future.
Welcome to DDFE GITHUB: github.com/DDFE wechat official account: wechat search the official account “DDFE” or scan the qr code below