Vue – the router source code parsing words | | 1.3 w multiple warning – [on]

  • Hello everyone, I am Guanghui 😎
  • vue-routerIs eachVue developersA plugin that is accessible to all
  • With the idea of going back to the source, I startedvue-routerPath to source code analysis 😂
  • I worked overtime every day, so it took months
  • Time to drag a very long, so there may be incoherent thinking of the situation, but also hope forgive 🤣
  • Due to the nuggets word limit, so divided into the next three to introduce
  • The first part of this paper mainly introduces the design ideas and basic principles of front-end routing.vue-routerRelated terms, directory structure, installation, instantiation, initialization related implementation
  • In the middle part, the mechanism of routing matching and navigation guard triggering are introduced
  • The next chapter is the final chapter, introducing some non-core features such as scroll processing,view,linkComponent implementation
  • The first time to do source code analysis, there must be a lot of mistakes or understanding is not in place, welcome to correct 🤞
  • The project address
    • https://github.com/BryanAdamss/vue-router-for-analysis
    • Give me one if you think it’ll helpstar
  • Uml diagram source file
    • https://github.com/BryanAdamss/vue-router-for-analysis/blob/dev/vue-router.EAP
  • Associated article link
    • Vue – the router source code parsing words | | 1.3 w multiple warning – [on]
    • Vue – the router source code parsing words | | 1.5 w multiple warning – 【 in 】
    • Vue – the router source code parsing | 6 k word – 【 the 】

Design ideas

  • In parsingvue-routerSo before we do this routing library, we need to know what is routing, what is front-end routing? What is the general approach to implementing front-end routing?

What is front-end routing

  • For the definition of routing, the wiki defines it like this;Routing is the activity of transferring information from a source address to a destination address over an interconnected network. Routing occurs at the third layer in the OSI network reference model, the network layer. Routing guides packet forwarding through a number of intermediate nodes to their final destination. The hardware is called a router. Routing typically guides packet forwarding according to a routing table -- a table stored with the best paths to each destination
  • The above definition may be official, but we can extract some important points
    • Routing is an activity that transfers information from a source address to a destination address.
    • To complete such an activity you need something very important – a routing table – a mapping of source and destination addresses
  • In web backend development, "route" refers to the corresponding handler assigned according to the URL.; Citing thehttps://www.zhihu.com/question/46767015@ royal ShiJun
    • The user enters a URL, the browser passes it to the server, the server matches the mapping table, finds the corresponding handler, and returns the corresponding resource (page or other);
  • For the front end, the routing concept came along with itspaThe emergence of; inspaBefore the emergence, the page jump (navigation) is controlled by the server, and there is an obvious white screen jump process;spaAfter the emergence, for a better experience, no longer let the server control the jump, so the front end routing emerged, the front end can freely control the rendering of components, to simulate the page jump
  • summary
    • Server routing basisurlAssign corresponding handler, return page, interface return
    • Front-end routing is throughjsAccording to theurlReturns the corresponding component

How to implement front-end routing

  • Looking at the above definition, to implement routing, you need one very important thing – a routing mapping table;
    • When the server switches to the routing page, the mapping table reflects the following informationurlandpageThe relationship between
    • Now the front end is basically modular, so the front end route mapping table reflectsurlandcomponentThe relationship between
  • Like the pseudocode below
// Use es6 map
const routeMap=new Map([['/', home page component], ['/bar',Bar component], ['/bar/foo', component Foo]])// You can also use object literals
const routeMap={
    '/': Home page component,'/bar': the Bar component,'/bar/foo'Components: Foo}Copy the code
  • So with the mapping table, we know thaturlMapping relationships with components;
  • However, the mapping maintains only a relationship, it doesn’t help us complete the access/barTo return toThe Bar componentSuch a process, so we also need a matcher, to help us complete fromurltocomponentMatching work;
  • Is there one?Route mapping tableandmatcherYou can implement front-end routing?
  • ourspaIt runs in the browser environment, and the browser doesForward, returnFunctional, we need to record the accessurl;
    • We know that to achieve this kind of similarityUndo, restoreA data structure must be used for the function ofThe stack (stack); Each visiturlThat will beurl.pushTo the stack, return, executepopCan get the last visiturl
    • Fortunately, the browser platform already provides such a stack, so we don’t need to implement it ourselves. We just need to call its window.history implementation
  • I drew a picture of the collaboration between the three
    • sequence.png
  • When we access a URL, for example/fooThe dispenser will take it/fooThe corresponding component is routed through the mapping table and returned to render, pushing the access record onto the history stack
  • When we access something by going forward/backwardurlIs first found in the history stackurlAnd then the dispenser takes iturlGo to the component and return to render; However, this is accessed forward/backward, so there is no need to push the history stack

conclusion

  • To implement a front-end route, there are three parts
    • Route mapping table
      • One that can expressurlandcomponentRelational mapping table that can be usedMap,Object literalsTo implement the
    • matcher
      • Responsible for visitingurl, to find out the correspondingcomponent
    • History stack
      • Browser platform, already native support, no implementation, direct call interface
  • You don’t have to worry about their implementation, you just need to know that there are three things and how they work together;
  • We’ll look at that latervue-routerHow to use them to achieve front-end routing

The term

  • Based on the analysis ofvue-routerBefore source code, we first understandvue-routerSome of the concepts often appear in terms, if it is difficult to understand, you can first skip, behind encounter, and then come back to see;

Routing rules, configuration objects (RouteConfig)

  • Route configuration item, used to describe the route
  • The red box in the following figure shows the routing configuration objects
  • route-config.png
  • becausevue-routerIs to supportEmbedded routines bySo configuration objects can also be nested with each other
  • The complete shape is shown below
interface RouteConfig = {
  path: string, component? : Component,// Routing componentname? :string.// Name the routecomponents? : { [name:string]: Component }, // Name the view componentredirect? :string | Location | Function, props? :boolean | Object | Function, alias? :string | Array<string>, children? :Array<RouteConfig>, // Nested bybeforeEnter? :(to: Route, from: Route, next: Function) = > void, meta? :any./ / server +caseSensitive? :boolean.// Is the matching rule case-sensitive? (Default: false)pathToRegexpOptions? :Object // Compile the re option
}
Copy the code

Routing record (RouteRecord)

  • Each routing rule generates a routing record. Nested and alias routes also generate a routing record. Is a component of the routing mapping table

 const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // Use the path-to-regexp package to generate enhanced regex objects to match path, which can be used to match dynamic routes
    components: route.components || { default: route.component }, / / save the routing module, support named view named view at https://router.vuejs.org/zh/guide/essentials/named-views.html#
    instances: {}, // Save the routing components that need to be rendered for each named router-view
    name,
    parent,
    matchAs,
    redirect: route.redirect, // Redirect the route configuration object
    beforeEnter: route.beforeEnter, // Route exclusive guard
    meta: route.meta || {}, / / meta information
    props// Dynamic route parameter transfer; https://router.vuejs.org/zh/guide/essentials/passing-props.html# routing components involved
      route.props == null
        ? {}
        : route.components // The parameter passing rule for the named view needs to use the rule specified by route.props
          ? route.props
          : { default: route.props }
  }
Copy the code

Routing object (Route)

  • RouteIndicates the status of the active route, including the current routeURLParsing the information, andURLMatched route recordsthe (route records).
    • https://router.vuejs.org/zh/api/# routing object
  • Notice, this tells you one thingRouteYou can associate more than oneRouteRecord
  • throughthis.$routeThat’s what I’m accessingRouteobject
  • route.png
  • The route object is immutable (immutableA new object is generated after each successful navigation

Location (Location)

  • It is notwindow.locationThe reference,vue-routerOne is defined internallyLocation, is an object that describes the location of a target;
  • $router.push/replace,The router - to linkReceiving isLocation object
    • https://router.vuejs.org/zh/api/#to
  • vue-routerThe interior can be aurl stringConverted toLocation objectSo to be exact$router.push/replace,The router - to linkIt’s always the sameRawLocation object
  • RawLocation objectisStringandLocationThe union type of
export type RawLocation = string | Location

export interfaceLocation { name? :stringpath? :stringhash? :stringquery? : Dictionary<string | (string | null|) []null | undefined> params? : Dictionary<string> append? :booleanreplace? :boolean
}
Copy the code

Routing component (RouteComponent)

  • When a route is matched successfully, the route must be stored in therouter-viewRender a component that needs to be renderedRouting component
  • RouteConfigIn theComponent, the components,Defined in theVue componentsThat’s the routing component
  • Specificity of routing components
    • haveThis parameter is valid only for routing componentsThe guard (BeforeRouteEnter, beforeRouteUpdate, beforeRouteLeave)
    • Have you ever called from a component, as I havebeforeRouteEnterThe discovery didn’t work because the guard could only work inRouting componentIs called in all non-routed components, including descendants of routed components; If you want to implement it in a routing componentbeforeRouteEnterSimilar to guard listening effect, can passwatch $routeTo manually determine
  • The red box isRouting component
  • route-component.png
  • After reading the above terms, you may still be in the clouds, nothing, will be explained in detail, now you just need to have a general understanding;

The environment

  • vue-routerVersion:v3.1.6
  • nodeVersion:v8.17.0
  • Address of analysis warehouse:https://github.com/BryanAdamss/vue-router-for-analysis
    • To highlight
      • Pay attention to seeThe commit record.commitRecorded the whole process of my analysis
      • If you feel ok, don’t forgetstar,fork🤞

The directory structure

  • First we are going tovue-routerwarehousecloneLet’s see what the directory structure looks like
  • git clone [email protected]:BryanAdamss/vue-router-for-analysis.git

directory

  • You can see the following directories
  • directory.png
  • Despite the catalogue, I’ve already highlighted it for you
  • We really only need to focus on the following directories or files
  • examples
    • It contains a carefully prepared official case
    • Not only tell youvue-routerIt also tells you how to deal with complex scenarios such as permission control and dynamic routing. Anyway, it’s worth checking out;
    • examples.png
    • In addition, we can use these examples for debugging when analyzing source code
      • Add source code where you want to debugDebugger Indicates a breakpointAnd then launch the examplenpm run devOpen,localhost:8080Can be
  • srcdirectory
    • This is avue-routerWhere the source code is stored is where we need to focus our attention
    • src.png
    • Pick up some important catalogs first
      • componentsThe directory is for storing built-in componentsrouter-link,router-viewthe
      • historyIs the storage coreThe history classThe place where
      • utilContains some helper functions
      • index.jsisvue-routerEntry file, alsovue-routerWhere the class is defined
      • install.jsIs the file where the installation logic resides
  • flow/declarations.js
    • It is avue-routerThe flow type declaration file, from which we can knowvue-routerWhat do core classes (objects) look like in
    • Inside it, it looks something like this
    • declarations.png

Based on example

  • Let’s start with the most basic example
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. Use plugin.
// Install the plug-in
Vue.use(VueRouter)

// 2. Define route components
// Define the routing component
const Home = { template'<div>home</div>' }
const Foo = { template'<div>foo</div>' }
const Bar = { template'<div>bar</div>' }

// 3. Create the router
// Instantiate vue-router
const router = new VueRouter({
  mode'history'.routes: [{path'/'.component: Home },
    { path'/foo'.component: Foo },
    { path'/bar'.component: Bar },
  ]
})

// 4. Create and mount root instance.
new Vue({
  router, // Inject the Router instance
  template` 
      

Basic

  • /
  • /foo
  • /bar
`
}).$mount('#app') Copy the code
  • As you can see, first usevueThe plugin syntax is installedvue-router; And then we instantiateVueRouter; And finally we are going toVueRouterIs injected into theVue
  • This really involves three core processes: installation, instantiation, and initialization
  • Let’s start with these three processes

The installation process

  • We know, as long as it’svue pluginThere must be oneinstallMethods;
    • Cn.vuejs.org/v2/guide/pl…
  • As mentioned abovevue-routerThe entry file is insrc/index.jsYes, let’s goindex.jsLook in theinstallmethods
// src/index.js

/* @flow */
import { install } from './install' // Import the installation method./ / VueRouter class
export default class VueRouter {... }... VueRouter.install = install// The install method is automatically called when vue. use is mounted
VueRouter.version = '__VERSION__'

// Browser environment, automatically install VueRouter
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
Copy the code
  • As you can see, it’s imported at the beginninginstallMethod and mount it directly as a static methodVueRouterGo on, like this, inVue.use(VueRouter)When,installThe method will be called;
  • You can see if in the browser environment, and throughThe script tagIntroduction of the form ofVue(inwindowOn the mountVueGlobal variables) will try to be used automaticallyVueRouter
  • Let’s seeinstall.jsWhat is the

install.js

  • installIt’s not that long

import View from './components/view'
import Link from './components/link'

export let _Vue // To avoid wrapping Vue as a dependency

/ / install method
export function install (Vue{
  if (install.installed && _Vue === Vue) return // Avoid repeated installation
  install.installed = true
  
  _Vue = Vue // Preserve Vue references
  
  const isDef = v= >v ! = =undefined
  
  // Associate the routing component with the router-view component
  const registerInstance = (vm, callVal) = > {
    let i = vm.$options._parentVnode
    / / call the vm $options. _parentVnode. Data. RegisterRouteInstance method
    // This method only exists in the router-view component, which is defined in (.. / components/view. Js @ 71 lines)
    // Therefore, if the parent node of the VM is router-view, the current VM is associated with the router-view, that is, the current VM is used as the routing component of the router-view
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  
  // Register global mixin
  Vue.mixin({
    beforeCreate () {
      // this === new Vue({router:router}) === Vue root instance
      
      // Check whether vue-router is used
      if (isDef(this.$options.router)) {
        // Save some information on the Vue root instance
        this._routerRoot = this // Save the Vue instance where VueRouter is mounted, which is the root instance
        this._router = this.$options.router // Save the VueRouter instance. This.$options.router only exists on the Vue root instance
        // beforeCreate is called when the beforeCreate hook is triggered
        this._router.init(this// Initialize the VueRouter instance and pass in the Vue root instance
        // Define the _route attribute responsively to ensure that the component is rerendered when the _route changes
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      } else {
        // Backtrack to _routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      
      // Associate the routing component with the router-view component
      registerInstance(this.this)
    },
    destroyed () {
      // Delete the association between router-view and routing components when destroyed hook is triggered
      registerInstance(this)}})// Add $router and $route attributes to the prototype
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    // Each component accessing $route ends up accessing _route of the Vue root instance
    get () { return this._routerRoot._route }
  })
  
  // Register router-view and router-link global components
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
  
  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
  • As you can see,installMethod does the following things

Avoid repeated installation

  • By addinginstalledFlag to determine whether repeated installation is possible

keepVueReference, avoid willVuePackaged as a dependency

  • installWhen the method is called, theVuePassed in as a parameter,VueIt’s going to be assigned to a predefined value_Vuevariable
  • In other modules, you can import this_Vue, so that you can accessVue, and avoid willVuePackaged as a dependency
  • This is a useful tip for plug-in development

A global mixin was registered

  • This mixin will affect every one created after registrationVueInstance, which is each of the followingVueInstances execute code that is mixed in
  • Let’s take a look at the code that’s been mixed in

  // Register global mixin
  Vue.mixin({
    beforeCreate () {
      // this === new Vue({router:router}) === Vue root instance
      
      // Check whether vue-router is used
      if (isDef(this.$options.router)) {
        // Save some information on the Vue root instance
        this._routerRoot = this // Save the Vue instance where VueRouter is mounted, which is the root instance
        this._router = this.$options.router // Save the VueRouter instance. This.$options.router only exists on the Vue root instance
        // beforeCreate is called when the beforeCreate hook is triggered
        this._router.init(this// Initialize the VueRouter instance and pass in the Vue root instance
        // Define the _route attribute responsively to ensure that the component is rerendered when the _route changes
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      } else {
        // Backtrack to _routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      
      // Associate the routing component with the router-view component
      registerInstance(this.this)
    },
    destroyed () {
      // Delete the association between router-view and routing components when destroyed hook is triggered
      registerInstance(this)}})Copy the code
  • It registers two lifecycle hooksbeforeCreateanddestroyed;
  • Pay attention to
    • Of the two hooks,thisRefers to the hook that was being calledVue instance;
    • The logic in these hooks is not executed during the installation process and is invoked only when the hook is executed during component instantiation
  • We see firstbeforeCreatehook
  • It judged firstthis.$options.routerDoes it exist? We arenew Vue({router})When,routerIt has been saved toVue root instancethe$optionsOn, and the othersVue instancethe$optionsThere was norouterthe
    • soifThe statement inthis === new Vue({router}), will be executed becauseVue root instanceThere is only one, so this logic will only be executed once
    • We can do it atifIn the printthisLook at it with a debugging tool
    • root-instance.png
    • Indeed,ifThe logic is executed only once, andthisIs the point toVue root instance
  • We look at theifWhat exactly did you do
    • Saved on the root instance_routerRoot, used to identifyrouterThe mountVue instance
    • Saved on the root instanceVueRouterInstance (router)
    • rightrouterInitialized (init)
      • routerThe initialization logic will be examined later
    • On the root instance, responsiveness is defined_routeattribute
      • ensure_routeChanges,router-viewIt will be re-rendered, which we’ll see laterrouter-viewMore on this in the component section
  • Let’s seeelseWhat exactly did
    • This is primarily defined for each component_routerRoot, is the use of layer by layer backtracking search
  • We see another oneregisterInstanceMethod, it’s inbeforeCreate,destroyedThey all get called, just with different numbers of arguments
    • inbeforeCreateTwo arguments are passed to thethisThe currentVue instance, and indestroyedOnly one is passed inVue instance
    • We are talking aboutrouter-viewWe’ll talk about it in detail, but you just need to know that it’s used forrouter-viewComponent association or unbindingRouting componentCan be used
    • Pass two parameters to associate, pass one parameter to unbind

Add instance properties, methods

  • inVueInjection on the prototype$router, $routeAttribute, convenient inVue instanceThrough thethis.$router,this.$routeQuick access to

Register router-view and router-link global components

  • throughVue.componentSyntax registeredrouter-viewandrouter-linkTwo global components

Set the merge policy for routing component guards

  • Of the routing componentbeforeRouteEnter,beforeRouteLeave,beforeRouteUpdateGuard merge strategy

conclusion

  • Let’s summarize the installation process with a diagram
  • install.png

Instantiation process

  • After looking at the installation process, let’s move onVueRouterThe instantiation process of
  • In this video, we’re going to focus on the instantiation process, so we’re just going to look atconstructorThe core logic in

Constructor of VueRouter

  • We opensrc/index.jsLook at theVueRouterThe constructor
// src/index.js

export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    this.app = null // Save the mount instance
    this.apps = [] // VueRouter supports multiple instances
    this.options = options
    this.beforeHooks = [] // Receive beforeEach hook
    this.resolveHooks = [] // Accept beforeResolve hook
    this.afterHooks = [] // Receive afterEach hook
    this.matcher = createMatcher(options.routes || [], this// Create a routing matcher object and pass in the Routes routing configuration list and VueRouter instance

    let mode = options.mode || 'hash'
    this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false // Whether to roll back
    if (this.fallback) {
      mode = 'hash'
    }

    // In a non-browser environment, enforce abstract mode
    if(! inBrowser) { mode ='abstract'
    }
    this.mode = mode

    // Instantiate different history instances according to different modes
    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
  • Constructors do a couple of things

Receive RouterOptions

  • As you can see, the constructor accepts oneOptions ObjectAnd its type isRouterOptionsLet’s seeRouterOptions
  • Open theflow/declarations.js
// flow/declarations.jsdeclare type RouterOptions = { routes? :Array<RouteConfig>;// List of route configuration rulesmode? : string;// Routing modefallback? : boolean;// Whether to enable rollbackbase? : string;/ / the base addresslinkActiveClass? : string;// Router-link activated class namelinkExactActiveClass? : string;// Router-link specifies the class name for precise activationparseQuery? :(query: string) = > Object; // Define the method of parsing QSstringifyQuery? :(query: Object) = > string; // Customize the method to serialize QSscrollBehavior? :(// Control the scrolling behavior
    to: Route,
    from: Route, savedPosition: ? Position) = > PositionResult | Promise<PositionResult>;
}

Copy the code
  • RouterOptionsDefines theVueRouterAll options that can be received;
    • Router.vuejs.org/zh/api/#rou…
  • Let’s focus on the following option values
    • routesIs the route configuration rule list, which is used to generate the route mapping table later.
      • It is an array, and each entry is a routing configuration rule (RouteConfig),RouteConfigRefer back to the terminology section;
    • mode,fallbackIt’s related to the routing mode
      • More on that laterVueRouterRouting mode of

Attribute assigns an initial value

  • Some attributes are assigned initial values, for example, to receiveGlobal navigation Guard (beforeEach, beforeResolve, afterEach)The array is initialized

Create the matcher

  • throughcreateMatcherTo generate thematcher
  • thisThe matcher objectIt’s the matcher we were talking about originally, which is responsible for URL matching, and it receives itroutesandThe router instances;createMatcherIt’s not just createdmatcher, also createdThe routing mapping table RouteMapWe’ll take a closer look later

Determine the routing mode

  • The three routing modes will be discussed later
  • Now we just need to knowVueRouterHow is the routing mode determined
  • VueRouterDepending on theoptions.mode,options.fallback,supportsPushState,inBrowserTo determine the final routing mode
  • Make sure thefallback.fallbackOnly if the user has set itmode:historyThe current environment does not support itpushStateAnd the user proactively declares that the rollback is requiredfallbackJust fortrue
  • whenFallback to trueWhen usinghashMode;
  • If it turns out to be in a non-browser environment, it is forcedabstractmodel
  • route-mode.png
  • inBrowserandsupportsPushStateThe implementation is simple
// src/util/dom.js
export const inBrowser = typeof window! = ='undefined' // Check whether window exists directly to determine whether it is in the browser environment

// src/util/push-state.js
export const supportsPushState =
  inBrowser && // Browser environment
  (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 // Special browsers do not support it directly
    }
    return window.history && typeof window.history.pushState === 'function' // Determine whether pushState is supported}) ()Copy the code

Generate different History instances based on routing patterns

  • Generate different routing patterns based on the previous stepThe History instance, about the routing mode,The History instanceBack to tell

summary

  • VueRouterThe constructor of the
    • To receive aRouterOptions
    • We then assign initial values to some attributes
    • To generate thematchermatcher
    • Determine the routing mode
    • Different routes are generated according to different routing modesThe History instance

Create matcher

  • Let’s take a closer lookcreateMatcherImplementation inside
  • createMatcherThe implementation of thesrc/create-matcher.jsIn the
 // src/create-matcher.js./ / the Matcher type
export type Matcher = {
  match(raw: RawLocation, current? : Route, redirectedFrom? : Location) = > Route; // Match method
  addRoutes: (routes: Array<RouteConfig>) = > void; // Add the Route method
};

// Matcher factory function
export function createMatcher (
  routes: Array<RouteConfig>, // Route configuration list
  router: VueRouter / / VueRouter instance
) :Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes) // Create a routing mapping table
  // Add a route
  function addRoutes (routes{
    // Since pathList, pathMap, nameMap were passed in, createRouteMap will perform the add logic
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // Pass location to return a matching Route object
  function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {... }// Return the Matcher object, exposing the match, addRoutes methods
  return {
    match,
    addRoutes
  }
} 
Copy the code
  • As you can see,createMatcherMethod receives a list of routing configuration rules andThe router instances, returns aMatcherobject
  • MatcherThe object contains one for matchingmatchMethod and a dynamic route adding methodaddRoutesMethods;
    • And both methods are declared increateMatcherInternally, because of the closure nature, it can accesscreateMatcherAll variables of the scope

summary

  • So let’s concludecreateMatcherThe logic of the
  • create-matcher.png
  • We can see thatcreateMatcherandaddRoutesMethodcreateRouteMapMethod, they just pass different parameters, from the name of the method, this method must andRouting table RouteMapThe relevant
  • Let’s seecreateRouteMapThe implementation of the

createRouteMap

  • createRouteMapMethods insrc/create-route-map.jsIn the

// Create a route mapping map and add a route record
export function createRouteMap (
  routes: Array<RouteConfig>, // Route configuration listoldPathList? :Array<string>, / / the old pathListoldPathMap? : Dictionary<RouteRecord>,/ / the old pathMapoldNameMap? : Dictionary<RouteRecord>/ / the old nameMap
) :{
  pathListArray<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {

  // If the old routing-related mapping list and map exist, use the old initialization (to enable adding routes)
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  
  // Traverses the route configuration object to generate/add route records
  routes.forEach(route= > {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  
  // ensure wildcard routes are always at the end
  // Make sure path:* is always at the end
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === The '*') {
      pathList.push(pathList.splice(i, 1) [0])
      l--
      i--
    }
  }
  
  // Development environment, indicating that non-nested path must start with/or *
  if (process.env.NODE_ENV === 'development') {
    // warn if routes do not include leading slashes
    const found = pathList
    // check for missing leading slash
      .filter(path= > path && path.charAt(0)! = =The '*' && path.charAt(0)! = ='/')
    if (found.length > 0) {
      const pathNames = found.map(path= > ` -${path}`).join('\n')
      warn(false.`Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)}}return {
    pathList,
    pathMap,
    nameMap
  }
}
Copy the code
  • You can seecreateRouteMapReturns an object that containspathList,pathMapandnameMap
  • pathListStored in theroutesAll of thepath
  • pathMapMaintenance ispathandRoute records RouteRecordThe mapping of
  • nameMapMaintenance isnameandRoute records RouteRecordThe mapping of
    • becauseVueRouterSupport named Routing
  • In the last two, it’s all about finding a quick matchRouting record
  • You can take a look and use the followingroutescallcreateRouteMapWhat will be returned
[{path'/'.component: Home },
    { path'/foo'.component: Foo ,
      children:[
      {
        path:'child'.component:FooChild
      }
    ]},
    { path'/bar/:dynamic'.component: Bar },
  ]
Copy the code
  • route-map-obj.png
  • There is no named route, sonameMapIs empty
  • pathListStore all of thempathOne of them is empty/In thenormalizePathWas deleted
  • pathMapRecorded every single one of thempathandCorresponding RouteRecordMapping relation of

summary

  • VueRouterThe routing mapping table is composed of three parts:pathList,pathMap,nameMap; The latter two are for quick lookups
  • createRouteMapThe logic of the
    • Check whether the routing mapping table already exists. If yes, use the mapping table. Otherwise, create a mapping table.
      • And that’s donecreateRouteMapDual functionality created/added
    • Then traverseroutes, each in turnroutecalladdRouteRecordTo generate aRouteRecordAnd update thepathList,pathMapandnameMap
    • Due to thepathListIn the future the logic will be used to traverse the match, for performance, so will be requiredpath:*Placed inpathListAt the end of the
    • Finally check the non-nested routinespathWhether to/or*At the beginning
  • It is summarized as follows
  • create-route-map-sequence.png
  • Next, how is the routing record generated

addRouteRecord

  • This method mainly creates the routing record and updates the routing mapping table
  • Located in thesrc/create-route-map.js
// src/create-route-map.js

// Add route records and update pathList, pathMap, and nameMap
function addRouteRecord (
  pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent? : RouteRecord,// Record the parent routematchAs? :string // Used when dealing with alias routes
{
  const { path, name } = route
  
  if(process.env.NODE_ENV ! = ='production') {
  
    // Route. path cannot be emptyassert(path ! =null.`"path" is required in a route configuration.`)
    
    // route.component cannot be string
    assert(
      typeofroute.component ! = ='string'.`route config "component" for path: The ${String(
        path || name
      )} cannot be a ` + `string id. Use an actual component instead.`)}const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
    
  // Generate a formatted path(the child route will concatenate the path of the parent route)
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
 
 // Is the matching rule case-sensitive? (Default: false)
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  
  // Generates a routing record
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // Use the path-to-regexp package to generate enhanced regex objects to match path, which can be used to match dynamic routes
    components: route.components || { default: route.component }, / / save the routing module, support named view named view at https://router.vuejs.org/zh/guide/essentials/named-views.html#
    instances: {}, // Save the router-view instance
    name,
    parent,
    matchAs,
    redirect: route.redirect, // Redirect the route configuration object
    beforeEnter: route.beforeEnter, // Route exclusive guard
    meta: route.meta || {}, / / meta information
    props// Dynamic route parameter transfer; https://router.vuejs.org/zh/guide/essentials/passing-props.html# routing components involved
      route.props == null
        ? {}
        : route.components // The parameter passing rule for the named view needs to use the rule specified by route.props
          ? route.props
          : { default: route.props }
  }
  
  // Handle child routing
  if (route.children) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    // https://github.com/vuejs/vue-router/issues/629
    // Named route && Does not use redirection && Child routing when path is '' or /, child routing will not be rendered when jumping with the name of the parent route
    if(process.env.NODE_ENV ! = ='production') {
      if( route.name && ! route.redirect && route.children.some(child= > ^ / / /? $/.test(child.path))
      ) {
        warn(
          false.`Named Route '${route.name}' has a default child route. ` +
            `When navigating to this named route (:to="{name: '${ route.name }'"), ` +
            `the default child route will not be rendered. Remove the name from ` +
            `this route and use the name of the default child route for named ` +
            `links instead.`)}}// Iterate over the generated child routing record
    route.children.forEach(child= > {
      const childMatchAs = matchAs // matchAs If it has the value, it indicates that the current route is an alias route. The child routes of the alias route need to be generated separately. The path prefix must be matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  
  // If the current path does not exist in pathMap, update pathList and pathMap
  if(! pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record }// Handle aliases; https://router.vuejs.org/zh/guide/essentials/redirect-and-alias.html#%E5%88%AB%E5%90%8D
  if(route.alias ! = =undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] // alias supports string, and Array< string >
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if(process.env.NODE_ENV ! = ='production' && alias === path) { // The value of alias is the same as that of path
        warn(
          false.`Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }
      
      // Generate an alias routing configuration object
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      
      // Add an alias route record
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute, // Alias route
        parent, // The parent route of the current route has the same parent route because the current route is named separately
        record.path || '/' // matchAs, used to generate child routes for alias routes;
      )
      / /! Summary: After the alias is set for the current route, the route records are generated for the current route and all its children. The path prefix of the children is matchAs(that is, the path of the alias route).}}// Handle named routes
  if (name) {
    / / update the nameMap
    if(! nameMap[name]) { nameMap[name] = record }else if(process.env.NODE_ENV ! = ='production' && !matchAs) {
      // Route duplicate warning
      warn(
        false.`Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`)}}}Copy the code
  • Look at the logic
    • The routing rules are checkedpathandcomponent
    • generatepath-to-regexpThe option topathToRegexpOptions
    • formattingpathIf it is nested, the parent route will be appendedpath
    • Generating Routing Records
    • Processing nested routines, recursive generation of child routing records
    • updatepathList,pathMap
    • Process alias routing and generate alias routing records
    • Handle named routes, updatesnameMap
  • Let’s look at some of the core logic

Generating Routing Records

  • Route records record core information about routes
  const record: RouteRecord = {
    path: normalizedPath,// Normalized path
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // Use the path-to-regexp package to generate enhanced regex objects to match path, which can be used to match dynamic routes
    components: route.components || { default: route.component }, / / save the routing module, support named view named view at https://router.vuejs.org/zh/guide/essentials/named-views.html#
    instances: {}, // Save the router-view instance
    name,
    parent,// Record the parent route
    matchAs, // Alias routing is required
    redirect: route.redirect, // Redirect the route configuration object
    beforeEnter: route.beforeEnter, // Route exclusive guard
    meta: route.meta || {}, / / meta information
    props// Dynamic route parameter transfer; https://router.vuejs.org/zh/guide/essentials/passing-props.html# routing components involved
      route.props == null
        ? {}
        : route.components // The parameter passing rule for the named view needs to use the rule specified by route.props
          ? route.props
          : { default: route.props }
  }
Copy the code
  • The routing record has aregexField, which is an enhanced regular expression, it is the key to achieve dynamic routing matching
  • regexIs through thecompileRouteRegexMethod, which is called insidepath-to-regexp
import Regexp from 'path-to-regexp'.// Use the path-to-regexp package to generate regex corresponding to route, which can be used to generate regular expressions for dynamic routes
function compileRouteRegex (
  path: string,
  pathToRegexpOptions: PathToRegexpOptions
) :RouteRegExp {

  const regex = Regexp(path, [], pathToRegexpOptions)
  if(process.env.NODE_ENV ! = ='production') {
    const keys: any = Object.create(null)
    regex.keys.forEach(key= > {
      // Repeat key extractwarn( ! keys[key.name],`Duplicate param keys in route with path: "${path}"`
      )
      keys[key.name] = true})}return regex
}
Copy the code
  • We look at thepath-to-regexpHow is it used
  • Website is: www.npmjs.com/package/pat…
  • Regexp takes three argumentsPath, keys, options;pathIs the path to be converted to regular,keysIs used to receive inpathFound in thekey, can be passed in or directly used on the return valuekeysProperties,optionsFor the options
  const pathToRegexp= require('path-to-regexp')
   
  const regexp = pathToRegexp("/foo/:bar");
  // regexp = /^\/foo\/([^\/]+?) / /? $/i
  // :bar is processed as a group of regees. When the re is executed, the value of bar can be retrieved from the group
  console.log(regexp.keys)  // keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }] 
Copy the code
  • Through the following example, we can know how to achieve the dynamic route to obtain parameter values
// test.js

const pathToRegexp= require('path-to-regexp')
   
const regexp = pathToRegexp("/foo/:bar");
 
console.log(regexp.keys)// Record key information

const m = '/foo/test'.match(regexp) // The regular group records value information

console.log('key:',regexp.keys[0].name,',value:',m[1])

Copy the code
  • path-to-regex-demo.png

Generate nested routines by recording

  • We know thatvue-routerInline routing is supported. Let’s see how to generate nested routing records
// src/create-route-map

 // Handle child routing
  if (route.children) {
    // Warn if route is named, does not redirect and has a default child route.
    // If users navigate to this route by name, the default child will
    // not be rendered (GH Issue #629)
    // https://github.com/vuejs/vue-router/issues/629
    // Named route && Does not use redirection && Child routing when path is '' or /, child routing will not be rendered when jumping with the name of the parent route
    if(process.env.NODE_ENV ! = ='production') {
      if( route.name && ! route.redirect && route.children.some(child= > ^ / / /? $/.test(child.path))
      ) {
        warn(
          false.`Named Route '${route.name}' has a default child route. ` +
            `When navigating to this named route (:to="{name: '${ route.name }'"), ` +
            `the default child route will not be rendered. Remove the name from ` +
            `this route and use the name of the default child route for named ` +
            `links instead.`)}}// Iterate over the generated child routing record
    route.children.forEach(child= > {
      const childMatchAs = matchAs // matchAs If it has the value, it indicates that the current route is an alias route. The child routes of the alias route need to be generated separately. The path prefix must be matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
Copy the code
  • First, in view of the# 629The question gives a warning
    • # 629The main problem is when a route isThe named route && does not use a redirection && subroute configuration object whose path is '' or /When the parent route is usednameWhen jumping, child routes will not be rendered
  • It then iterates through the list of child routing rules to generate child routing records
    • It also handles the child routing of alias routes
      • If the parent route is marked as an alias route, the child route ispathThe parent route needs to be added in front of itpathAnd then regenerate it into a record
  • We can look at the following nested pattern, what does the generated routing mapping table look like
[{path'/parent'.component: Parent,
      children: [{path'foo'.component: Foo }, 
      ]
    }
 ]
Copy the code
  • nested-route.png
  • As you can see, subpathspathThe parent route is appended before the sessionpath

Generate alias routing records

  • VueRouterAliases for routes are supported./aThe alias is/bMeans when the user accesses/bWhen,URLWill stay for/b, but the route match is/a, like user access/aThe same
  • How do peppers generate alias routing records
// src/create-route-map.js

  if(route.alias ! = =undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] // alias supports string, and Array< string >
    
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if(process.env.NODE_ENV ! = ='production' && alias === path) { // The value of alias is the same as that of path
        warn(
          false.`Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }
      
      // Generate an alias routing configuration object
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      
      // Add an alias route record
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute, // Alias route
        parent, // The parent route of the current route has the same parent route because the current route is named separately
        record.path || '/' // matchAs, used to generate child routes for alias routes;
      )
      / /! Summary: After the alias is set for the current route, the route records are generated for the current route and all its children. The path prefix of the children is matchAs(that is, the path of the alias route).}}Copy the code
  • An alias supports single and multiple aliases, that isroute.aliasSupport the incoming/fooor['/foo','/bar'], so the two cases were normalized and unified into an array
  • It then iterates through the array, checking aliases andpathWhether to duplicate, then generate a separate configuration pass for the alias route, and finally calladdRouteRecordGenerate alias routing records
    • Notice that it also passesmatchAsHandles scenarios where aliased routes generate child routes, mainly through SettingsmatchAsforrecord.path || '/', and then generates child routing records based onmatchAsGenerates a child record of the alias routing record, as shown aboveNested routines consist of chapters
  • What does an alias route map look like
[{path'/root'.component: Root, alias'/root-alias' },
    { path'/home'.component: Home,
      children: [{path'bar'.component: Bar, alias'bar-alias' },
        { path'baz'.component: Baz, alias: ['/baz'.'baz-alias']}]}Copy the code
  • alias-route.png
  • You can see that a separate routing record is generated for both the alias route and the child routes of the alias route

summary

  • The routing record isRoute mapping tableIs an important part of
    • In the routing recordregexIs a key field used to process dynamic route parameterspath-to-regexpImplementation of the
  • Routing records are generated based on the following types of routing records
    • Embedded routines by
      • A child route generates a single route record
    • Alias routing and alias routing subroutes
      • An alias route and its subroutes each generate a routing record
    • After routing
  • The entire process for generating routing records is shown in the following figure
  • add-route-record.png
  • So we’re done hereVueRouterInstantiation to create matchers (generate routing mapping tables) related logic
  • Next we’ll look at generation according to routing patternsThe History instanceRelated logic of

Routing patterns

  • An important feature of front-end routing is to switch pages without refreshing
    • That is, the URL changes, the page does not refresh to realize the page jump
  • There are two ways to achieve this
  • A kind ofhash+hashChange“, another useHistory APIthepushState+popState
  • The former mainly useshashThe page does not refresh and fires when changes are madehashChangeThis feature implements front-end routing
  • The latter made full use of itHTML5 History API thepushStateMethods andpopStateEvent to implement front-end routing
  • The comparison
  • The former
    • Good compatibility,hashChangeSupport to IE8
    • urlWill carry/ # /Not beautiful,
    • No server side modification is required
  • The latter
    • Compatible to the consumer
    • urlWith normalurlThe same
    • Due to itsurlWith normalurlSame, so when you refresh, it’s going to use thisurlRequest server page for link, and the server does not have this page, will 404, so need to cooperate with the server to redirect all requests to the home page, the control of the whole route to the front-end route
  • VueRouterThree routing modes are supportedhash,history,abstract
  • hashA pattern is an implementation of the first scenario
  • historyA pattern is an implementation of the second scenario
  • abstractMode is used in non-browser environments, mainly forSSR

Core classes

  • VueRouterThree routing modes are implemented by the following three core classes
  • History
    • The base class
    • Located in thesrc/history/base.js
  • HTML5History
    • Used to supportpushStateThe browser
    • src/history/html5.js
  • HashHistory
    • Used when not supportedpushStateThe browser
    • src/history/hash.js
  • AbstractHistory
    • For non-browser environments (server-side rendering)
    • src/history/abstract.js
  • You can see the relationship between them in the graph below
  • route-mode-class.png
  • HTML5History,HashHistory,AbstractHistoryAll three inherit from the base classHistory;
  • The three are not only accessibleThe History classThey also implement the five interfaces declared in the base class that require subclass implementation (Go, Push, REPLACE, ensureURL, getCurrentLocation)
  • Due to theHashHistoryListening to thehashChangeSo there will be one moresetupListenersmethods
  • AbstractHistoryBecause it needs to be used in a non-browser environment, there is no history stack, so it can only be passedThe index, the stackTo simulate the
  • In the previous analysisVueRouterWhen you instantiate the process, you know thatVueRouterDifferent routing modes will be instantiated after they are determinedThe History instance
  • So let’s see the differenceHistoryThe instantiation process of

The History class

  • It is the parent class (base class) from which all other classes inherit
  • The code is located in thesrc/history/base.js
// src/history/base.js
/ / parent class
export class History {
  router: Router
  basestring
  current: Route
  pending: ?Route
  cb(r: Route) = > void
  readyboolean
  readyCbsArray<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  
  // implemented by sub-classes
  // Need subclasses (HTML5History, HashHistory) to implement the method
  +go: (n: number) = > void
  +push: (loc: RawLocation) = > void
  +replace: (loc: RawLocation) = > void
  +ensureURL: (push? :boolean) = > void
  +getCurrentLocation: () = > string
  
  constructor (router: Router, base: ?string) {
    this.router = router
    // Format base to ensure that base begins with a slash
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START // The route object currently pointed to, default is START; Namely the from
    this.pending = null // Record the route to jump to; That is to
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }
  
  ...
  
}
Copy the code
  • As you can see, the constructor does the following things
    • Save therouterThe instance
    • Standardization of thebaseTo ensure thatbaseBased on/At the beginning
    • Initializes the current route pointing, default onlySTARTInitial routing; When a route jumps,this.currentRepresents thefrom
    • Initialize the next route during route jump. The default route isnull; When a route jumps,this.pendingRepresents theto
    • Some callback-related properties are initialized
  • STARTDefined in thesrc/utils/route.jsIn the
// src/utils/route.js
// Initial route
export const START = createRoute(null, {
  path'/'
})
Copy the code
  • The History classIs instantiated as follows
  • history.png

HTML5History class

  • Let’s seeHTML5History classIt is inherited fromThe History class
  • Located in thesrc/history/html5.js

export class HTML5History extends History {
  constructor (router: Router, base: ?string) {
    // Initialize the parent class History
    super(router, base)
    
    // Check whether scroll needs to be supported
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    
    // If scroll is supported, initialize the scroll logic
    if (supportsScroll) {
      setupScroll()
    }
    
    // Get the initial location
    const initLocation = getLocation(this.base)
    
    // Listen for popState events
    window.addEventListener('popstate'.e= > {
      const current = this.current
      
      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      // In some browsers, popState is triggered once when the page is opened
      // If the initial route is asynchronous, 'popState' will be triggered first, and the initial route will be parsed after completion, resulting in the route not updated
      // So avoid
      const location = getLocation(this.base)
      if (this.current === START && location === initLocation) {
        return
      }
      
      // If the route address changes, the router jumps, and the scroll is processed after the jump
      this.transitionTo(location, route= > {
        if (supportsScroll) {
          handleScroll(router, route, current, true)}})})}}Copy the code
  • You can see that it inherits fromThe History class, so the superclass constructor is called in the constructor (super(router,base))
  • Checks whether scrolling behavior needs to be supported and initializes the rolling-related logic if so
  • Listen to thePopstate eventAnd, inpopstateInvokes when triggeredtransitionToMethod to implement a jump
  • Notice that an exception scenario is handled here
    • In some browsers, opening the page will trigger oncepopstate, if the routing component is asynchronouspopstateThe event is raised, but the asynchronous component is not finished parsing, resulting inrouteThere is no update
    • So we’re masking that
  • There will be a section on scrolling and routing jumps later
  • HTML5History classIs instantiated as follows
  • h5history.png

HashHistory class

  • Located in thesrc/history/hash.js
// src/history/hash.js

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    // Instantiate the parent class
    super(router, base)
    // check history fallback deeplinking
    // Fallback is true only when mode is history, the browser does not support popState, and the user manually specifies fallback as true. Otherwise, fallback is false
    // If rollback is required, change the URL to hash mode (beginning with /#)
    // this.base comes from the parent class
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
}
Copy the code
  • It is inherited fromHistory, so also calledsuper(router,base)
  • Check thefallbackTo see if I need to back up, as we said, the incomingfallbackOnly if the user has set ithistoryAnd they don’t support itpushStateThis parameter is valid only when rollback is enabledtrue
  • So, at this point, you need to puthistoryPatterns ofurlreplacehashMode, that is, add on#The logic is based oncheckFallbackImplementation of the
  • If it is notfallbackIs directly calledensureSlashTo ensure thaturlBased on/At the beginning of
  • We look at thecheckFallback,ensureSlashimplementation
// src/history/hash.js

/** * check the rollback to convert the URL to hash mode (add /#) */
function checkFallback (base{
  const location = getLocation(base)
  // If the address does not start with /#, add it
  if (!/ # ^ / / /.test(location)) {
    window.location.replace(cleanPath(base + '/ #' + location)) // This step implements URL substitution
    return true}}/** * make sure the URL starts with /* /
function ensureSlash () :boolean {
  const path = getHash()
  if (path.charAt(0) = = ='/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

// Replace hash records
function replaceHash (path{
  // If pushState is supported, replaceState is preferred
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
Copy the code
  • Did you find thatHashHistoryLess scrolling support and listeninghashChangeRelevant logic, and that’s becausehashChangeThere are special scenarios that need to waitmountsBefore they can listen in.
    • The logic of this piece is all insetupListenersMethod,setupListenersWill be inVueRoutercallinit“, which we’ll look at in the initialization section
  • HashHistory classIs instantiated as follows
  • hash-history.png

AbstractHistory class

  • AbstractHistoryIs intended for non-browser environments
  • Located in thesrc/history/abstract.js
// src/history/abstract.js

/** * Supports all JavaScript runtime environments, such as node.js server. If no browser API is found, the route will automatically force the mode * *@export
 * @class AbstractHistory
 * @extends {History}* /
export class AbstractHistory extends History {
 
  constructor (router: Router, base: ?string) {
    // Initialize the parent class
    super(router, base)
    this.stack = []
    this.index = -1}}Copy the code
  • As you can see, its instantiation is the simplest, only the parent class is initialized and theindex,stackInitialization is done
  • As mentioned earlier, in non-browser environments, there is no history stack, so useindex,stackTo simulate the history stack
  • AbstractHistory classIs instantiated as follows
  • abstract-history.png

summary

  • In this section we briefly analyze the three routing patterns and look at what is required to implement themHistoryHow is a class instantiated
  • To this,VueRouterThe entire instantiation process is basically covered
  • Now, let’s summarize it briefly with a diagramVueRouterInstantiation process
  • vue-router-instance.png

Initialization Process

  • Now that we’ve analyzed the instantiation process, let’s look at how the initialization works. What did you do

When to call init

  • When analyzing the installation process, we know thatVueRouterRegistered a global blend, blendbeforeCreatehook
  • The following code
// src/install.js

// Register global mixin
Vue.mixin({
  beforeCreate () {
    
    // Check whether vue-router is used
    if (isDef(this.$options.router)) {
     ...
      this._router = this.$options.router // Save the VueRouter instance. This.$options.router only exists on the Vue root instance
      // beforeCreate is called when the beforeCreate hook is triggered
      this._router.init(this// Initialize the VueRouter instance and pass in the Vue root instance
    } else{... }... }, destroyed () { ... }})Copy the code
  • We know that global mixing affects all subsequent creationVue instance, sobeforeCreateThe first trigger was inThe Vue root instance is instantiatednamelynew Vue({router})When,

After the router instance is triggered, the init method of the router instance is called and the Vue root instance is passed to complete the initialization process.

  • Due to therouterExists only inVue root instancethe$optionsSo, the entire initialization will only be called once
  • So let’s seeinitMethod implementation

The init method

  • VueRoutertheinitMethods insrc/index.js
// src/install.js
export default class VueRouter {
  
  // Initialize app as Vue root instance
  init (app: any /* Vue component instance */) {
    
    // Development environment, make sure you have VueRouter installedprocess.env.NODE_ENV ! = ='production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )
    
    this.apps.push(app) // Save the instance
    
    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    // Bind destroyed hook to avoid memory leaks
    app.$once('hook:destroyed'.() = > {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1this.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
      // Make sure there is always a master application
      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 the main app already exists, there is no need to repeat the initialization of the history event listener
    if (this.app) {
      return
    }
    
    this.app = app
    
    const history = this.history
    
    if (history instanceof HTML5History) {
      // In the case of HTML5History, the parent class's transitionTo method is called directly to the current location
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
    
      // In the case of HashHistory, onComplete, onAbort callback is passed after calling the parent class transitionTo method
      const setupHashListener = () = > {
        / / call HashHistory setupListeners method, set the hashchange listening in
        // Set the hashchange listener after route switch.
        / / repair at https://github.com/vuejs/vue-router/issues/725
        // Because beforeEnter hooks are fired twice if the hook function beforeEnter is asynchronous. Since #/ is added to hash values that do not begin with a/at initialization, the hashchange event is triggered and the lifecycle hook is traversed again, which means the beforeEnter hook function is called again.
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener, // onComplete callback to transitionTo
        setupHashListener // onAbort callback of transitionTo)}// Call the parent class's LISTEN method to add the callback;
    // The callback is triggered when the parent class's updateRoute method is called, reassigning app._route
    // Since app._route is defined as responsive, app._route changes and components that depend on app._route (route-View components) are re-rendered
    history.listen(route= > {
      this.apps.forEach((app) = > {
        app._route = route
      })
    })
    
  }
} 
Copy the code
  • As you can see, the main things are as follows
  • Check theVueRouterHas been installed?
  • The mount is savedThe router instancestheVue instance
    • VueRouterSupports multi-instance nesting, so it existsthis.appsTo save holdThe router instancestheVue instance
  • Registered a one-time hookdestroyedIn thedestroyedWhen unloadingthis.appTo avoid memory leaks
  • Check thethis.appTo avoid repeated event listening
  • According to thehistoryType, calltransitionToJump to a different initial page
  • registeredupdateRouteThe callback, inrouterUpdate when updatedapp._routeComplete the page re-rendering
    • This is what we’re doingThe view componentsWe’ll talk about that in more detail
  • Let’s focus on thattransitionToRelevant logic

setupListeners

  • It says that, at initialization, it’s based onThe history type, the calltransitionToJump to a different initial page
  • Why jump to the initial page?
    • Because the URL may point to another page during initialization, you need to call itgetCurrentLocationMethod resolves the path from the current URL and jumps to it
  • You can seeHTML5History classandHashHistory classcalltransitionToMethods have different parameters
    • The former takes only one argument
    • The latter takes three arguments
  • We look at thetransitionToMethod’s method signature

// src/history/base.js./ / parent class
export class History {...// Route jump
  transitionTo (
    location: RawLocation, // Primitive location, either a URL or a location interface(custom shape, defined in types/router.d.ts)onComplete? :Function.// The jump successfully callbackonAbort? :Function// Jump failed callback) {... }}Copy the code
  • The first parameter is the address to resolve, the second is the jump success callback, and the third is the jump failure callback
  • Let’s seeHashHistoryWhy does a class need to pass in a callback
  • You can see that both the success and failure callbacks passed in aresetupHashListenerThe function,setupHashListenerThe function is called internallyhistory.setupListenersMethod, and this method isHashHistory classThe unique
  • Open thesrc/history/hash.js
// src/history/hash.js

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  / / repair # 725; https://github.com/vuejs/vue-router/issues/725
  // Because beforeEnter hooks are fired twice if the hook function beforeEnter is asynchronous. Since #/ is added to hash values that do not begin with a/at initialization, the hashchange event is triggered and the lifecycle hook is traversed again, which means the beforeEnter hook function is called again.
  setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    // If scroll is supported, initialize the scroll logic
    if (supportsScroll) {
      setupScroll()
    }
    // Add event listener
    window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange'.// PopState is preferred
      () = > {
        const current = this.current
        if(! ensureSlash()) {return
        }
        this.transitionTo(getHash(), route= > {
          if (supportsScroll) {
            handleScroll(this.router, /* to*/route, /* from*/current, true)}// Do not support pushState, directly replace records
          if(! supportsPushState) { replaceHash(route.fullPath) } }) } ) }Copy the code
  • The main logic is as follows
  • setupListenersIt mainly determines whether the rolling behavior needs to be supported, and if so, initializes the related logic
  • And then addChange the urlEvent listening. I said there are two ways to implement routingpushState+popState,hash+hashChange
  • You can see here even though it’sHashHistoryWill also be used preferentiallypopstateEvent to listenurlThe change of the
  • whenurlIs called when changes occurtransitionToJump to a new route
  • You can see the logical sum of thisHTML5History classThe logic handled at instantiation time is similar
// src/history/html5.js

export class HTML5History extends History {
  constructor (router: Router, base: ?string) {...// Check whether scroll needs to be supported
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    // If scroll is supported, initialize the scroll logic
    if (supportsScroll) {
      setupScroll()
    }

    // Get the initial location
    const initLocation = getLocation(this.base)
    // Listen for popState events
    window.addEventListener('popstate'.e= >{...// If the route address changes, the router jumps, and the scroll is processed after the jump
      this.transitionTo(location, route= > {
        if (supportsScroll) {
          handleScroll(router, route, current, true)}})})}}Copy the code
  • So why is the timing different?
    • HTML5HistroyListen for events at instantiation time
    • HashHistoryListen for events after the initial route jump ends
  • This is for repair# 725 problem
    • ifbeforeEnterIf it’s asynchronous,beforeEnterIt fires twice, and that’s because at initialization,hashValue is not/The first words will be filled in# /, the process will triggerhashchangeEvent, so the lifecycle hook goes through again, causing the call againbeforeEnterHook function. So you have to puthashChangeEvent monitoring is delayed until the initial route jump is complete.

summary

  • The following activity diagram is summarized for the init process
  • init.png

conclusion

  • This paper mainly starts from the whole design idea of front-end routing, analyzes the basic principles of front-end routing design step by step, and clarifies the design idea
  • And then I introduced the globalvue-routerSeveral terms
  • Having a general idea of the terminology, we went over itvue-routerHow is the directory structure layered
  • Once you understand the directory structure, let’s start with installationvue-routerWhat was done during installation
  • After the installation, we introduced a few moreThe History classWhat is the inheritance relationship and how is it instantiated
  • With the instantiation done, we finally walk through the initialization process

PS

  • The rest will be introduced later. If you think it’s ok, you can give it a thumbs up at ✨
  • personalgithubgithub.com/BryanAdamss, also summed up a few things, welcome star
    • Drawing -board based on Canvas
    • Front-end introduction Demo, best practices collection FE-awesome -demos
    • One that automatically generates aliasesvue-cli-pluginwww.npmjs.com/package/vue…
  • 🏆 nuggets technical essay | double festival special articles

reference

  • Github.com/dwqs/blog/i…
  • Github.com/dwqs/blog/i…
  • Github.com/dwqs/blog/i…
  • Github.com/vuejs/vue-r…
  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 684490…
  • www.jianshu.com/p/29e8214d0…
  • Ustbhuangyi. Making. IO/vue – analysi…
  • Blog.liuyunzhuge.com/2020/04/08/…

communication

  • If you have any problems, you can add wechat to communicate and grow together and make progress together