VueRouter4 Github address: github.com/vuejs/vue-r… This article is based on release V4.0.8

I. Analysis of import files

The entry file for the source code is usually found in the package.json file scripts:

{
    "scripts": {
        "build": "rollup -c rollup.config.js"."dev": "webpack serve --mode=development". }}Copy the code

In the scripts configuration of the source code, the build command tells you the following:

  • Production version approvedrollupBuild tools for packaging
  • Can be achieved byrollup.config.jsFile Obtains information about the import file

SRC /index.ts: SRC /index.ts: SRC /index.ts: SRC /index.ts: SRC /index.ts:

{
    input: `src/index.ts`,}Copy the code

Two, the import file export module

Open the entry file SRC /index.ts, the source code is as follows:

export { createWebHistory } from './history/html5'
export { createMemoryHistory } from './history/memory'
export { createWebHashHistory } from './history/hash'
export { createRouterMatcher, RouterMatcher } from './matcher'

export {
  LocationQuery,
  parseQuery,
  stringifyQuery,
  LocationQueryRaw,
  LocationQueryValue,
  LocationQueryValueRaw,
} from './query'

export { RouterHistory, HistoryState } from './history/common'

export { RouteRecord, RouteRecordNormalized } from './matcher/types'

export {
  PathParserOptions,
  _PathParserOptions,
} from './matcher/pathParserRanker'

export {
  routeLocationKey,
  routerViewLocationKey,
  routerKey,
  matchedRouteKey,
  viewDepthKey,
} from './injectionSymbols'

export {
  // route location
  _RouteLocationBase,
  LocationAsPath,
  LocationAsRelativeRaw,
  RouteQueryAndHash,
  RouteLocationRaw,
  RouteLocation,
  RouteLocationNormalized,
  RouteLocationNormalizedLoaded,
  RouteParams,
  RouteParamsRaw,
  RouteParamValue,
  RouteParamValueRaw,
  RouteLocationMatched,
  RouteLocationOptions,
  RouteRecordRedirectOption,
  // route records
  _RouteRecordBase,
  RouteMeta,
  START_LOCATION_NORMALIZED as START_LOCATION,
  RouteComponent,
  // RawRouteComponent,
  RouteRecordName,
  RouteRecordRaw,
  NavigationGuard,
  NavigationGuardNext,
  NavigationGuardWithThis,
  NavigationHookAfter,
} from './types'

export {
  createRouter,
  Router,
  RouterOptions,
  RouterScrollBehavior,
} from './router'

export {
  NavigationFailureType,
  NavigationFailure,
  isNavigationFailure,
} from './errors'

export { onBeforeRouteLeave, onBeforeRouteUpdate } from './navigationGuards'
export {
  RouterLink,
  useLink,
  RouterLinkProps,
  UseLinkOptions,
} from './RouterLink'
export { RouterView, RouterViewProps } from './RouterView'

export * from './useApi'
export * from './globalExtensions'
Copy the code

By analyzing the content of export, the exported API can be divided into the following categories:

  • The history module
  • The matcher module
  • The router module
  • RouterLink module
  • RouterView module
  • Errors module
  • NavigationGuards module
  • other
    • injectionSymbols
    • types
    • useApi
    • globalExtensions

The following sections will start with a step-by-step analysis of the basic functions and implementation principles of the above modules, from the basics to the advanced ones in VueRouter’s documentation.

Third, based

Using document corresponding address: next.router.vuejs.org/zh/guide/

CreateRouter creates a route instance

In the sample code provided with the document, the definition of the route configuration is basically unchanged from VueRouter3, but the route instance is created by executing the createRouter(options) method.

const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
const routes = [
  { path: '/'.component: Home },
  { path: '/about'.component: About },
]
const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes,
})
Copy the code

The createRouter method is exported from the Router module. The router module source path is SRC /router.ts. In this file, find the createRouter method source. In short, this method simply passes in an object of type RouterOptions and returns a Router instance.

 export function createRouter(options: RouterOptions) :Router {
  / /... Left out a bunch of code...
  const router: Router = {
    currentRoute,

    addRoute,
    removeRoute,
    hasRoute,
    getRoutes,
    resolve,
    options,

    push,
    replace,
    go,
    back: () = > go(-1),
    forward: () = > go(1),

    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,

    onError: errorHandlers.add,
    isReady,

    install(app: App) {
      // ...}},return router
}
Copy the code

2. Parameters: RouterOptions

The createRouter() method has only one options object argument of type RouterOptions.

// src/router.ts
export interface RouterOptions extends PathParserOptions {
  history: RouterHistory
  routes: RouteRecordRaw[] scrollBehavior? : RouterScrollBehavior parseQuery? :typeoforiginalParseQuery stringifyQuery? :typeoforiginalStringifyQuery linkActiveClass? :stringlinkExactActiveClass? :string
}
Copy the code

As you can see from the interface definition, the object attributes that must be included are:

  • History: indicates the history of route implementation. The type isRouterHistory.
  • Routes: indicates the initial route list that should be added to the route. The type isRouteRecordRaw.

The following attributes are not required:

  • ScrollBehavior: Function that controls scrolling while navigating between pages. You can return aPromiseTo delay scrolling.
  • ParseQuery: A custom implementation for parsing queries. The query key and value must be decoded. See correspondingstringifyQuery.
  • StringifyQuery: Custom implementation of stringing a query object. I shouldn’t have put? . The query key and value should be properly encoded.parseQueryCorresponds to processing query resolution.
  • LinkActiveClass: Default class for activating the RouterLink. If nothing is provided, will it be usedrouter-link-active.
  • LinkExactActiveClass: Default class for precise activation of the RouterLink. If nothing is provided, will it be usedrouter-link-exact-active.

Let’s analyze the history and routes attributes.

2-1, the history

The interfaces to RouterHistory are defined as follows:

interface RouterHistory {
  // The read-only attribute, the base path, is added to the front of each URL
  readonly base: string
  // Read-only attribute, current route
  readonly location: HistoryLocation
  // Read-only property, current state
  readonly state: HistoryState
  // Route jump methodpush(to: HistoryLocation, data? : HistoryState):void
  // Route jump methodreplace(to: HistoryLocation, data? : HistoryState):void
  // Route jump method
  go(delta: number, triggerListeners? :boolean) :void
  // Add a route event listener
  listen(callback: NavigationCallback): () = > void
  // Generate the href method used in the anchor tag
  createHref(location: HistoryLocation): string
  / / remove listeners
  destroy(): void
}
Copy the code

VueRouter provides three ways to create a RouterHistory object:

  • CreateWebHashHistory (): Creates a hash history. This is useful for web applications without a host (such as file://), or when the configuration server cannot handle arbitrary urls. Note: If SEO is important to you, you should use createWebHistory.

  • CreateWebHistory (): Create an HTML5 history, which is the most common history in a single-page application. Applications must be serviced over the HTTP protocol.

  • CreateMemoryHistory () : Creates a memory-based history. The main purpose of this history is to deal with SSR. It starts in a special place, a place that is everywhere. If users are not in the browser context, they can replace that location with the start location by calling router.push() or router.replace().

In other words, when creating an instance of VueRouter, the options.history parameter is one of the above three options or a custom method (which requires returning a RouterHistory object).

1, createWebHashHistory

(1) base

In the example provided above, the createWebHashHistory method is called with no arguments and the access address is http://localhost:8080, so base is ‘/’ and there is no #, so base is appended with a # symbol. The createWebHistory function is then called to continue creating additional properties or methods.

base = location.host ? base || location.pathname + location.search : ' '
if (base.indexOf(The '#') < 0) 
    base += The '#'
return createWebHistory(base)
Copy the code

(2) Other attributes and methods

All attributes and methods except the Base attribute are created using the createWebHistory(base) method, so other attributes and methods are analyzed in createWebHistory(base).

2, createWebHistory

(1) Base attribute

NormalizeBase = ‘/#’; normalizeBase = ‘/#’; normalizeBase = ‘/#’

If the VueRouter instance is created when createWebHistory() is called, then base will be undefined and normalizeBase will be an empty string “”.

base = normalizeBase(base)
Copy the code

The normalizeBase method code is as follows:

// src/utils/env.ts
export const isBrowser = typeof window! = ='undefined'

// src/history/common.ts
function normalizeBase(base? :string) :string {
  if(! base) {if (isBrowser) {
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      base = base.replace(/^\w+:\/\/[^\/]+/.' ')}else {
      base = '/'}}if (base[0]! = ='/' && base[0]! = =The '#') base = '/' + base
  return removeTrailingSlash(base)
}

// src/location.ts
const TRAILING_SLASH_RE = / / / $/
export const removeTrailingSlash = (path: string) = > path.replace(TRAILING_SLASH_RE, ' ')
Copy the code

(2) Creation of other properties and methods

In createWebHistory approach, by calling the useHistoryStateNavigation (base) method, return a contains the location, the state, a push, the replace object properties and methods.

// src/history/html5.ts
const historyNavigation = useHistoryStateNavigation(base)
Copy the code

Then call useHistoryListeners(…) Function, return pauseListeners, listen, destroy method of object.

const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location,
    historyNavigation.replace
  )
Copy the code

Next declare the go() method:

function go(delta: number, triggerListeners = true) {
    // ...
}
Copy the code

Then combine the default objects and objects from the above two methods into a routerHistory object. The routerHistory object is created.

// src/history/html5.ts
const routerHistory: RouterHistory = assign(
  {
    // it's overridden right after
    location: ' ',
    base,
    go,
    createHref: createHref.bind(null, base),
  },

  historyNavigation,
  historyListeners
)
Copy the code

Finally, add the getter for the location and state properties. When reading the values of these properties, return the value of the object’s value property.

Object.defineProperty(routerHistory, 'location', {
  enumerable: true.get: () = > historyNavigation.location.value,
})

Object.defineProperty(routerHistory, 'state', {
  enumerable: true.get: () = > historyNavigation.state.value,
})
Copy the code

Hash and History routing modes use the same logic for all attributes or methods except base. Now that you know the overall process of creating a RouterHistory object, you can look at the implementation logic for properties or methods other than base.

  • location

    The location attribute is in useHistoryStateNavigation () method, the statement of the method is related to the location of the code from the simplified as shown below.

    function useHistoryStateNavigation(base: string) {
      const { location } = window
      let currentLocation: ValueContainer<HistoryLocation> = {
        value: createCurrentLocation(base, location),
      }
      return {
        location: currentLocation,
      }
    }
    
    function createCurrentLocation(
      base: string,
      location: Location
    ) :HistoryLocation {
      const { pathname, search, hash } = location
      // Support hash such as #, /#, #/, #! The #! / / #! /, or/folder# end
      const hashPos = base.indexOf(The '#')
      // If it is a hash
      if (hashPos > -1) {
        let slicePos = hash.includes(base.slice(hashPos))
          ? base.slice(hashPos).length
          : 1
        let pathFromHash = hash.slice(slicePos)
        // prepend the starting slash to hash so the url starts with /#
        if (pathFromHash[0]! = ='/') pathFromHash = '/' + pathFromHash
        return stripBase(pathFromHash, ' ')}const path = stripBase(pathname, base)
      return path + search + hash
    }
    
    function stripBase(pathname: string, base: string) :string {
      // There is no base or base cannot be found at the start
      if(! base || pathname.toLowerCase().indexOf(base.toLowerCase()))return pathname
      return pathname.slice(base.length) || '/'
    }
    Copy the code

    We declare the internal property currentLocation, which is an object with only one value property. The value of value is obtained by createCurrentLocation(Base, location). This method formats the current URL as a standard path + search + hash string.

  • state

    State property is also in useHistoryStateNavigation method statement, the code associated with the state as follows.

    function useHistoryStateNavigation(base: string) {
      const { history } = window
      let historyState: ValueContainer<StateEntry> = { value: history.state }
    
      return {
        state: historyState,
      }
    }
    Copy the code

    The state property is the object for which window.history.state is the value of the value property.

    After you create and declare the state attribute, you need to make the following judgments.

    If this value is false, no operation is performed on browser history. In this case, you need to call the changeLocation method. The changeLocation method is important as it is the basis for push and route hop methods such as replace. This method takes three parameters: the target location, the target state object, and whether to replace the current location.

    if(! historyState.value) { changeLocation( currentLocation.value, {back: null.current: currentLocation.value,
          forward: null.position: history.length - 1.replaced: true.scroll: null,},true)}let createBaseLocation = () = > location.protocol + '/ /' + location.host
    
    function changeLocation(
      to: HistoryLocation,
      state: StateEntry,
      replace: boolean
    ) :void {
      const hashIndex = base.indexOf(The '#')
      const url =
        hashIndex > -1
          ? (location.host && document.querySelector('base')
              // base + currentLocation.value
              ? base
              // #xxx + currentLocation.value
              : base.slice(hashIndex)) + to
          // 'url' is: 'protocol :// Host address + base + currentLocation.value';
          : createBaseLocation() + base + to
      try {
        // Try changing window.history using the history API
        history[replace ? 'replaceState' : 'pushState'](state, ' ', url)
        historyState.value = state
      } catch (err) {
        // If using the history API fails, downgrade to window.location instead
        location[replace ? 'replace' : 'assign'](url)
      }
    }
    Copy the code
  • The replace method

    The replace method is also in useHistoryStateNavigation statement, related to the source code is as follows, the replace method receives the to and the data parameter, first by building a state object parameters, and then call routed changeLocation method to jump.

    function useHistoryStateNavigation(base: string) {
      const { history } = window
      function replace(to: HistoryLocation, data? : HistoryState) {
        // Integrate the state object
        const state: StateEntry = assign(
          {},
          history.state,
          buildState(
            historyState.value.back,
            to,
            historyState.value.forward,
            true
          ),
          data,
          { position: historyState.value.position }
        )
        // Call the route jump method
        changeLocation(to, state, true)
        currentLocation.value = to
      }
    
      return {
        replace,
      }
    }
    
    const computeScrollPosition = () = >
      ({
        left: window.pageXOffset,
        top: window.pageYOffset,
      } as _ScrollPositionNormalized)
    
    function buildState(
      back: HistoryLocation | null,
      current: HistoryLocation,
      forward: HistoryLocation | null,
      replaced: boolean = false,
      computeScroll: boolean = false
    ) :StateEntry {
      return {
        back,
        current,
        forward,
        replaced,
        position: window.history.length,
        scroll: computeScroll ? computeScrollPosition() : null,}}Copy the code
  • Push method

    Push method and replace method types, the relevant source is as follows.

    function useHistoryStateNavigation(base: string) {
      const { history } = window
    
      function push(to: HistoryLocation, data? : HistoryState) {
        const currentState = assign(
          {},
          historyState.value,
          history.state as Partial<StateEntry> | null,
          {
            forward: to,
            scroll: computeScrollPosition(),
          }
        )
    
        changeLocation(currentState.current, currentState, true)
    
        const state: StateEntry = assign(
          {},
          buildState(currentLocation.value, to, null),
          { position: currentState.position + 1 },
          data
        )
    
        changeLocation(to, state, false)
        currentLocation.value = to
      }
    
      return {
        push,
      }
    }
    Copy the code

    Look at the push method and see that it calls the changeLocation method twice internally. Why is that?

    The main reason is that the scroll and forward information corresponding to the current location need to be saved. In this case, the replace parameter of the changeLocation method is true, that is, the current location needs to be updated.

    The state of the page to jump to is then created. The postion value is +1, and the replace parameter is false when the changeLocation method is called.

  • Go way

    The go method is declared directly in the createWebHistory method, calling window.history.go(delta) for a route jump. Additional support triggerListeners parameter, the default triggers the listeners in the callback function, if introduced into false, call the historyListeners. PauseListeners (), the method modifies the pauseState variables, This variable will be referred to later in the popState event.

    function go(delta: number, triggerListeners = true) {
        if(! triggerListeners) historyListeners.pauseListeners() history.go(delta) }Copy the code
    function pauseListeners() {
        pauseState = currentLocation.value
    }
    Copy the code
  • Listen method

    The Listen method is returned by useHistoryListeners. Check the source code of the Listen method in useHistoryListeners.

    function useHistoryListeners() {
      let listeners: NavigationCallback[] = []
      let teardowns: Array<() = > void> = []
    
      function listen(callback: NavigationCallback) {
        listeners.push(callback)
    
        const teardown = () = > {
          const index = listeners.indexOf(callback)
          if (index > -1) listeners.splice(index, 1)
        }
    
        teardowns.push(teardown)
        return teardown
      }
    
      return {
        listen,
      }
    }
    Copy the code

    The Listen method adds the callbacks passed to the Listeners array, returns the listeners’ removal function, and adds the removal function to the Teardowns array for bulk removal.

  • CreateHref method

    This method binds to the public createHref method in the createWebHistory method, creates a createHref method with a base argument, and returns an href composed of the current base when the createHref method in the routerHistory method is called.

    // src/history/html5.ts
    function createWebHistory(base? :string) :RouterHistory {
        // omit other code
        const routerHistory: RouterHistory = assign(
            {
              // omit other code
              createHref: createHref.bind(null, base),
            },
            // omit other code
        )
        // omit other code
    }
    Copy the code
    // src/history/common.ts
    // remove any character before the hash
    const BEFORE_HASH_RE = / ^ ^ # # + /
    export function createHref(base: string, location: HistoryLocation) :string {
      return base.replace(BEFORE_HASH_RE, The '#') + location
    }
    Copy the code
  • Destroy methods

    The destroy method is built on useHistoryListeners.

    function useHistoryListeners() {
      let teardowns: Array<() = > void> = []
    
      const popStateHandler: PopStateListener = ({
        state,
      }: {
        state: StateEntry | null
      }) = > {
        // ...
      }
    
      function beforeUnloadListener() {
        // ...
      }
    
      function destroy() {
        for (const teardown of teardowns) teardown()
        teardowns = []
        window.removeEventListener('popstate', popStateHandler)
        window.removeEventListener('beforeunload', beforeUnloadListener)
      }
    
      window.addEventListener('popstate', popStateHandler)
      window.addEventListener('beforeunload', beforeUnloadListener)
    
      return {
        destroy,
      }
    }
    Copy the code

    The destroy method is called by iterating through the Teardowns array, removing all listeners, then emptying the Teardowns array, and untying the popState and beforeUnload events on the Window object.

    The popState event is triggered when the active history entry changes, and the popStateHandler function is called, the source code for which is shown below.

    const popStateHandler: PopStateListener = ({
      state,
    }: {
      state: StateEntry | null
    }) = > {
      const to = createCurrentLocation(base, location)
      const from: HistoryLocation = currentLocation.value
      const fromState: StateEntry = historyState.value
      let delta = 0
    
      if (state) {
        currentLocation.value = to
        historyState.value = state
    
        // ignore the popstate and reset the pauseState
        if (pauseState && pauseState === from) {
          pauseState = null
          return
        }
        delta = fromState ? state.position - fromState.position : 0
      } else {
        replace(to)
      }
    
      listeners.forEach(listener= > {
        listener(currentLocation.value, from, {
          delta,
          type: NavigationType.pop,
          direction: delta
            ? delta > 0
              ? NavigationDirection.forward
              : NavigationDirection.back
            : NavigationDirection.unknown,
        })
      })
    }
    Copy the code

    This function is invoked when the user manipulates the browser navigation button or when methods such as push/replace/ Go are called in the application. The key to this function, in addition to updating some object values, is to traverse the Listeners array to call each registered callback function.

    PauseState = null pauseState = null pauseState = null pauseState = null pauseState = null pauseState = null pauseState = null pauseState = null pauseState = null pauseState = null pauseState = null Return statements, so we don’t continue the logic of the listeners.

    Beforeunload Event Triggered when the browser window closes or refreshes, the beforeUnloadListener function is called. This function uses the following source code to save the current scrolling information to the current history entity.

    function beforeUnloadListener() {
      const { history } = window
      if(! history.state)return
      history.replaceState(
        assign({}, history.state, { scroll: computeScrollPosition() }),
        ' ')}Copy the code

CreateWebHashHistory createWebHashHistory createWebHashHistory createWebHashHistory From the above analysis, it can be concluded that the difference between history and hash mode lies in the processing of base, in other words, the difference in browser URL representation. Route jump and event monitoring are based on the HISTORY API. However, when there is an error in using history to jump, VueRouter is fault-tolerant and demotes to jump using location.

3, createMemoryHistory

The createMemoryHistory function does not have a window. History object because it does not run on the browser. Now look directly at how the properties and methods in the routerHistory object are implemented.

  • Base Specifies the base parameter. The default value is ‘/’.

  • The initial value of location is the constant START. Each time location is retrieved, the latest location value is retrieved from queue[position].

    // src/history/common.ts
    const START: HistoryLocation = ' '
    
    // src/history/memory.ts
    const routerHistory: RouterHistory = {
        // rewritten by Object.defineProperty
        location: START,
    }
    Object.defineProperty(routerHistory, 'location', {
        enumerable: true.The queue array emulates the browser history
        get: () = > queue[position],
    })
    Copy the code
  • State defaults to an empty object state: {}, but TODO is commented in the source code and should be changed in subsequent versions.

    // TODO: should be kept in queue
    state: {},
    Copy the code
  • The push method, in short, appends a location to a queue array.

    let queue: HistoryLocation[] = [START]
    function setLocation(location: HistoryLocation) {
      position++
      if (position === queue.length) {
        // we are at the end, we can simply append a new entry
        queue.push(location)
      } else {
        // we are in the middle, we remove everything from here in the queue
        queue.splice(position)
        queue.push(location)
      }
    }
    const routerHistory: RouterHistory = {
      push(to, data? : HistoryState) {
        setLocation(to)
      },
    }
    Copy the code
  • The replace method is the same as the push method in that replace is called to delete the last location in the queue, and then setLocation is called to append another location.

    const routerHistory: RouterHistory = {
      replace(to) {
        // remove current entry and decrement position
        queue.splice(position--, 1)
        setLocation(to)
      },
    }
    Copy the code
  • The go method takes the delta parameter size and then updates the value of position.

    There’s no setLocation method called, so we don’t update the queue, we update the position, and then we get the location from queue[position], so we get the correct location.

    function triggerListeners(
      to: HistoryLocation,
      from: HistoryLocation,
      { direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>
    ) :void {
      const info: NavigationInformation = {
        direction,
        delta,
        type: NavigationType.pop,
      }
      for (let callback of listeners) {
        callback(to, from, info)
      }
    }
    
    const routerHistory: RouterHistory = {
      go(delta, shouldTrigger = true) {
        const from = this.location
        const direction: NavigationDirection =
          // we are considering delta === 0 going forward, but in abstract mode
          // using 0 for the delta doesn't make sense like it does in html5 where
          // it reloads the page
          delta < 0 ? NavigationDirection.back : NavigationDirection.forward
        position = Math.max(0.Math.min(position + delta, queue.length - 1))
        if (shouldTrigger) {
          triggerListeners(this.location, from, {
            direction,
            delta,
          })
        }
      },
    }
    Copy the code
  • The Listen method does much the same thing on the browser side. When a method is called, it adds a callback function passed in to the listeners array and returns a function to delete it.

    let listeners: NavigationCallback[] = []
    const routerHistory: RouterHistory = {
      listen(callback) {
        listeners.push(callback)
        return () = > {
          const index = listeners.indexOf(callback)
          if (index > -1) listeners.splice(index, 1)}}}Copy the code
  • The createHref method is implemented the same way on the browser side.

    const routerHistory: RouterHistory = {
      createHref: createHref.bind(null, base),
    }
    Copy the code
  • Destroy method This method, when called, resets route-related variables.

    const routerHistory: RouterHistory = {
      destroy() {
        listeners = []
        queue = [START]
        position = 0}},Copy the code

    Note the teardowns array is missing on the web browser, but the Listeners can be cleaned up directly on the web browser. 🤔

History properties analysis to this end, through the history of three methods for creating the analysis, we learned the browser side routing related properties, methods and events, and the browser side is how to implement the browser routing simulation, in which memory type feel there is need to improve, the follow-up version should have some changes.

2-2, routes attribute

Back in the createRouter method, you can see that options.routes is used in only one place in the method. It serves as the createRouterMatcher parameter and returns an object of type RouterMatcher.

export function createRouter(options: RouterOptions) :Router {
  const matcher = createRouterMatcher(options.routes, options)
  / /...
}
Copy the code

The entry to the matcher module is SRC /matcher/index.ts. This module provides routing configuration related properties and methods. The matcher interface is defined as follows.

// src/matcher/index.ts
interface RouterMatcher {
  addRoute: (record: RouteRecordRaw, parent? : RouteRecordMatcher) = > () = > void
  removeRoute: {
    (matcher: RouteRecordMatcher): void
    (name: RouteRecordName): void
  }
  getRoutes: () = > RouteRecordMatcher[]
  getRecordMatcher: (name: RouteRecordName) = > RouteRecordMatcher | undefined
  resolve: (location: MatcherLocationRaw, currentLocation: MatcherLocation) = > MatcherLocation
}
Copy the code

1. Basic logic of createRouterMatcher function

The simplified code looks like this.

function createRouterMatcher(routes: RouteRecordRaw[], globalOptions: PathParserOptions) :RouterMatcher {
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  globalOptions = mergeOptions(
    { strict: false.end: true.sensitive: false } as PathParserOptions,
    globalOptions
  )

  function getRecordMatcher(name: RouteRecordName) {
    // ...
  }

  function addRoute(record: RouteRecordRaw, parent? : RouteRecordMatcher, originalRecord? : RouteRecordMatcher) {
    // ...
  }

  function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
    // ...
  }

  function getRoutes() {
    // ...
  }

  function insertMatcher(matcher: RouteRecordMatcher) {
    // ...
  }

  function resolve(location: Readonly
       
        , currentLocation: Readonly
        
       ) :MatcherLocation {
    // ...
  }

  // add initial routes
  routes.forEach(route= > addRoute(route))

  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
Copy the code

This function takes two arguments, the first being the route configuration array and the second being the options passed in when VueRouter was initialized. We then declare two variables matchers and matcherMap. We then declare a series of methods. Before returning, we traverse routes and convert the route configuration to matcher by using the addRoute method.

Let’s look at each of these methods one by one.

  • AddRoute method

    function addRoute(record: RouteRecordRaw, parent? : RouteRecordMatcher, originalRecord? : RouteRecordMatcher) {
      letisRootAdd = ! originalRecordlet mainNormalizedRecord = normalizeRouteRecord(record)
      mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
      const options: PathParserOptions = mergeOptions(globalOptions, record)
      const normalizedRecords: typeof mainNormalizedRecord[] = [
        mainNormalizedRecord,
      ]
      if ('alias' in record) {
        const aliases =
          typeof record.alias === 'string' ? [record.alias] : record.alias!
        for (const alias of aliases) {
          normalizedRecords.push(
            assign({}, mainNormalizedRecord, {
              components: originalRecord
                ? originalRecord.record.components
                : mainNormalizedRecord.components,
              path: alias,
              aliasOf: originalRecord
                ? originalRecord.record
                : mainNormalizedRecord,
            }) as typeof mainNormalizedRecord
          )
        }
      }
    
      let matcher: RouteRecordMatcher
      let originalMatcher: RouteRecordMatcher | undefined
    
      for (const normalizedRecord of normalizedRecords) {
        let { path } = normalizedRecord
        if (parent && path[0]! = ='/') {
          let parentPath = parent.record.path
          let connectingSlash =
            parentPath[parentPath.length - 1= = ='/' ? ' ' : '/'
          normalizedRecord.path =
            parent.record.path + (path && connectingSlash + path)
        }
        matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
        if (originalRecord) {
          originalRecord.alias.push(matcher)
        } else {
          originalMatcher = originalMatcher || matcher
          if(originalMatcher ! == matcher) originalMatcher.alias.push(matcher)if(isRootAdd && record.name && ! isAliasRecord(matcher)) removeRoute(record.name) }if ('children' in mainNormalizedRecord) {
          let children = mainNormalizedRecord.children
          for (let i = 0; i < children.length; i++) {
            addRoute(
              children[i],
              matcher,
              originalRecord && originalRecord.children[i]
            )
          }
        }
        originalRecord = originalRecord || matcher
        insertMatcher(matcher)
      }
    
      return originalMatcher
        ? () = > {
            removeRoute(originalMatcher!)
          }
        : noop
    }
    Copy the code

    All this does is create a matcher object based on the route configuration object, add it to the Matchers array, and return a remove route method or noop based on the originalMatcher condition (let noop = () => {}). The route configuration transmitted in the application is incomplete. Therefore, you need to format the route configuration using the normalizeRouteRecord method to generate a complete route configuration object. The props properties are formatted using the normalizeRecordProps function. The formatting object is generated based on the Component or components of the route configuration object. If there is a Component attribute, the props object contains a default attribute and is assigned to the props in the configuration. Otherwise, the key of the Components object is used. And take the corresponding value from the route configuration property props.

    function normalizeRouteRecord(
      record: RouteRecordRaw
    ) :RouteRecordNormalized {
      return {
        path: record.path,
        redirect: record.redirect,
        name: record.name,
        meta: record.meta || {},
        aliasOf: undefined.beforeEnter: record.beforeEnter,
        props: normalizeRecordProps(record),
        children: record.children || [],
        instances: {},
        leaveGuards: new Set(),
        updateGuards: new Set(),
        enterCallbacks: {},
        components:
          'components' in record
            ? record.components || {}
            : { default: record.component! }}},function normalizeRecordProps(
      record: RouteRecordRaw
    ) :Record<string._RouteRecordProps> {
      const propsObject = {} as Record<string, _RouteRecordProps>
      const props = (record as any).props || false
      if ('component' in record) {
        propsObject.default = props
      } else {
        for (let name in record.components)
          propsObject[name] = typeof props === 'boolean' ? props : props[name]
      }
    
      return propsObject
    }
    Copy the code

    After the normalizeRouteRecord method is called to format the route configuration object, the processed mainNormalizedRecord object is added to the normalizedRecords array. If there is an alias, add the record to the normalizedRecords array. The basic logic is to copy the mainNormalizedRecord and reset the components, path, aliasOf properties. In other words, Aliasing works by copying records and adjusting some properties to get a new record. The above code is a preparation for the creation of a matcher. Continue to analyze the code by first preparing two variables: matcher and originalMatcher, and then iterating through normalizedRecords.

    Here’s a trick. Matcher assigns values during traversal, so why not put them inside the traversal? This is because if put inside, each traversal will generate a new object, if the routing number, will create a temporary object in a short period of time often, causes memory footprint, may cause frequent garbage collection, eventually leading to page caton, so on the outside of the traversal, can reduce the number of temporary variables, optimize the memory footprint, Reduce the number of garbage collections.

    Inside the traversal, according to the route configuration object, create matcher, and insert into matchers, divided into the following steps:

    • 1. If a child route is configured and path does not start with a slash (/), add the path of the parent route and the path of the child route to generate the complete path

    • 2. Call createRouteRecordMatcher to create a matcher object. If parent exists, add the current matcher object to parent-children.

      function createRouteRecordMatcher(
        record: Readonly<RouteRecord>,
        parent: RouteRecordMatcher | undefined, options? : PathParserOptions) :RouteRecordMatcher {
        const parser = tokensToParser(tokenizePath(record.path), options)
        const matcher: RouteRecordMatcher = assign(parser, {
          record,
          parent,
          children: [].alias: [],})if (parent) {
          if(! matcher.record.aliasOf === ! parent.record.aliasOf) parent.children.push(matcher) }return matcher
      }
      Copy the code

      The matcher object is of type RouteRecordMatcher, which inherits from the PathParser interface, so a matcher object should contain the following properties and methods, The first five properties or methods are created by tokensToParser(tokenizePath(Record.path), options). The implementation logic of these properties or methods will be analyzed in the method below.

      • re: RegExp
      • score: Array<number[]>
      • keys: PathParserParamKey[]
      • parse(path: string): PathParams | null
      • stringify(params: PathParams): string
      • record: RouteRecordSave the formatted route configuration records
      • parent: RouteRecordMatcher | undefinedSave the parent route matcher object
      • children: RouteRecordMatcher[]Child route, initialized to an empty array
      • alias: RouteRecordMatcher[]Alias, initialized to an empty array

      Before analyzing tokensToParser, we need to take a look at tokenizePath(Record.path), which converts path to a token array.

      export const enum TokenType {
        Static,
        Param,
        Group,
      }
      
      const enum TokenizerState {
        Static,
        Param,
        ParamRegExp, // custom re for a param
        ParamRegExpEnd, // check if there is any ? + *
        EscapeNext,
      }
      
      interface TokenStatic {
        type: TokenType.Static
        value: string
      }
      
      interface TokenParam {
        type: TokenType.Param regexp? :string
        value: string
        optional: boolean
        repeatable: boolean
      }
      
      interface TokenGroup {
        type: TokenType.Group
        value: Exclude<Token, TokenGroup>[]
      }
      
      export type Token = TokenStatic | TokenParam | TokenGroup
      
      const ROOT_TOKEN: Token = {
        type: TokenType.Static,
        value: ' ',}const VALID_PARAM_RE = /[a-zA-Z0-9_]/
      // After some profiling, the cache seems to be unnecessary because tokenizePath
      // (the slowest part of adding a route) is very fast
      
      // const tokenCache = new Map<string, Token[][]>()
      
      export function tokenizePath(path: string) :Array<Token[] >{
        if(! path)return [[]]
        if (path === '/') return [[ROOT_TOKEN]]
        if(! path.startsWith('/')) {
          throw new Error(
            __DEV__
              ? `Route paths should start with a "/": "${path}" should be "/${path}". `
              : `Invalid path "${path}"`)}// if (tokenCache.has(path)) return tokenCache.get(path)!
      
        function crash(message: string) {
          throw new Error(`ERR (${state})/"${buffer}": ${message}`)}let state: TokenizerState = TokenizerState.Static
        let previousState: TokenizerState = state
        const tokens: Array<Token[]> = []
        // the segment will always be valid because we get into the initial state
        // with the leading /
        letsegment! : Token[]function finalizeSegment() {
          if (segment) tokens.push(segment)
          segment = []
        }
      
        // index on the path
        let i = 0
        // char at index
        let char: string
        // buffer of the value read
        let buffer: string = ' '
        // custom regexp for a param
        let customRe: string = ' '
      
        function consumeBuffer() {
          if(! buffer)return
      
          if (state === TokenizerState.Static) {
            segment.push({
              type: TokenType.Static,
              value: buffer,
            })
          } else if (
            state === TokenizerState.Param ||
            state === TokenizerState.ParamRegExp ||
            state === TokenizerState.ParamRegExpEnd
          ) {
            if (segment.length > 1 && (char === The '*' || char === '+'))
              crash(
                `A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`
              )
            segment.push({
              type: TokenType.Param,
              value: buffer,
              regexp: customRe,
              repeatable: char === The '*' || char === '+'.optional: char === The '*' || char === '? '})},else {
            crash('Invalid state to consume buffer')
          }
          buffer = ' '
        }
      
        function addCharToBuffer() {
          buffer += char
        }
      
        while (i < path.length) {
          char = path[i++]
      
          if (char === '\ \'&& state ! == TokenizerState.ParamRegExp) { previousState = state state = TokenizerState.EscapeNextcontinue
          }
      
          switch (state) {
            case TokenizerState.Static:
              if (char === '/') {
                if (buffer) {
                  consumeBuffer()
                }
                finalizeSegment()
              } else if (char === ':') {
                consumeBuffer()
                state = TokenizerState.Param
              } else {
                addCharToBuffer()
              }
              break
      
            case TokenizerState.EscapeNext:
              addCharToBuffer()
              state = previousState
              break
      
            case TokenizerState.Param:
              if (char === '(') {
                state = TokenizerState.ParamRegExp
              } else if (VALID_PARAM_RE.test(char)) {
                addCharToBuffer()
              } else {
                consumeBuffer()
                state = TokenizerState.Static
                // go back one character if we were not modifying
                if(char ! = =The '*'&& char ! = ='? '&& char ! = ='+') i--
              }
              break
      
            case TokenizerState.ParamRegExp:
              // TODO:is it worth handling nested regexp? like :p(? :prefix_([^/]+)_suffix)
              // it already works by escaping the closing )
              // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
              // is this really something people need since you can also write
              // /prefix_:p()_suffix
              if (char === ') ') {
                // handle the escaped )
                if (customRe[customRe.length - 1] = ='\ \')
                  customRe = customRe.slice(0, -1) + char
                else state = TokenizerState.ParamRegExpEnd
              } else {
                customRe += char
              }
              break
      
            case TokenizerState.ParamRegExpEnd:
              // same as finalizing a param
              consumeBuffer()
              state = TokenizerState.Static
              // go back one character if we were not modifying
              if(char ! = =The '*'&& char ! = ='? '&& char ! = ='+') i--
              customRe = ' '
              break
      
            default:
              crash('Unknown state')
              break}}if (state === TokenizerState.ParamRegExp)
          crash(`Unfinished custom RegExp for param "${buffer}"`)
      
        consumeBuffer()
        finalizeSegment()
      
        // tokenCache.set(path, tokens)
      
        return tokens
      }
      Copy the code

      The purpose of this function is to convert the path string to an array for subsequent processing. For example, /user will be converted to [[{type: 0, value: ‘user’}]] and /user/:id will be converted to:

      [[{type: 0.value: "user"}],
          [{type: 1.value: "id".regexp: "".repeatable: false.optional: false}]]Copy the code

      Go back to the tokensToParser function and analyze how PathParser is generated.

      • re

        A regular expression that converts tokens into regular expressions that match the path using tokens passed in from parameters and a list of criteria.
        const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
          sensitive: false.strict: false.start: true.end: true,}function tokensToParser(
          segments: Array<Token[]>, extraOptions? : _PathParserOptions) :PathParser {
          const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)
          let pattern = options.start ? A '^' : ' '
          for (const segment of segments) {
              // Iterate through tokens to improve regular expressions
              // TODO: Dig a hole here and analyze how to generate regular expressions later
          }
          if(! options.strict) pattern +='/? '
          if (options.end) pattern += '$'
          else if (options.strict) pattern += '(? : / | $) '
          const re = new RegExp(pattern, options.sensitive ? ' ' : 'i')
          // ...
        }
        Copy the code
      • score

        Calculate a score for the current path, and use the score value to compare subsequent paths, which is equivalent to comparing weights.
        let score: Array<number> = [] []for (const segment of segments) {
            const segmentScores: number[] = segment.length ? [] : [PathScore.Root]
            // ...
            score.push(segmentScores)
        }
        if (options.strict && options.end) {
            const i = score.length - 1
            score[i][score[i].length - 1] += PathScore.BonusStrict
        }
        Copy the code
      • keys

        Saves the dynamic parameters of a route.
        const keys: PathParserParamKey[] = []
        for (const segment of segments) {
            // ...
            if (token.type === TokenType.Param) {
                const { value, repeatable, optional, regexp } = token
                keys.push({
                  name: value,
                  repeatable,
                  optional,
                })
            }
            // ...
        }
        Copy the code
      • parse

        Pass in the path argument, then get the dynamic argument object based on the RE, and then iterate over the result.
        function parse(path: string) :PathParams | null {
            const match = path.match(re)
            const params: PathParams = {}
        
            if(! match)return null
        
            for (let i = 1; i < match.length; i++) {
              const value: string = match[i] || ' '
              const key = keys[i - 1]
              params[key.name] = value && key.repeatable ? value.split('/') : value
            }
        
            return params
        }
        Copy the code
      • stringify

        This method passes in the params object and returns the path of the parameter object combined with path instead of the parameter value.
        function stringify(params: PathParams) :string {
            let path = ' '
            // for optional parameters to allow to be empty
            let avoidDuplicatedSlash: boolean = false
            for (const segment of segments) {
              if(! avoidDuplicatedSlash || ! path.endsWith('/')) path += '/'
              avoidDuplicatedSlash = false
        
              for (const token of segment) {
                if (token.type === TokenType.Static) {
                  path += token.value
                } else if (token.type === TokenType.Param) {
                  const { value, repeatable, optional } = token
                  const param: string | string[] = value in params ? params[value] : ' '
        
                  if (Array.isArray(param) && ! repeatable)throw new Error(
                      `Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`
                    )
                  const text: string = Array.isArray(param) ? param.join('/') : param
                  if(! text) {if (optional) {
                      // if we have more than one optional param like /:a? -static we
                      // don't need to care about the optional param
                      if (segment.length < 2) {
                        // remove the last slash as we could be at the end
                        if (path.endsWith('/')) path = path.slice(0, -1)
                        // do not append a slash on the next iteration
                        else avoidDuplicatedSlash = true}}else throw new Error(`Missing required param "${value}"`)
                  }
                  path += text
                }
              }
            }
        
            return path
        }
        Copy the code

      It’s not easy to go through these complicated steps and get a complete Matcher object.

    • The originalMatcher property is then assigned to the originalMatcher as matcher if it is the first assignment. The matcher is not reassigned, but added to the OriginalRecord. alias array.

    • 4, then according to the ‘children’ in mainNormalizedRecord conditions determine whether zi lu by, if there is zi lu by the traversal mainNormalizedRecord. Children array, and call addRoute method, the parameters are: Children [I], Matcher, originalRecord && originalRecord. Children [I].

    • 5. Finally call insertMatcher(matcher) to add matcher to matchers and update the matcherMap.

      function insertMatcher(matcher: RouteRecordMatcher) {
        let i = 0
        while (
          i < matchers.length &&
          comparePathParserScore(matcher, matchers[i]) >= 0
        )
          i++
        matchers.splice(i, 0, matcher)
        if(matcher.record.name && ! isAliasRecord(matcher)) matcherMap.set(matcher.record.name, matcher) }Copy the code

    The addRoute method is completed.

  • The resolve method returns the MatcherLocation object, which contains the following attributes: The function of name, path, params, matched and meta is to perform route matching according to the incoming location and find the routing information corresponding to the matcher corresponding to the location.

    function resolve(location: Readonly
             
              , currentLocation: Readonly
              
             ) :MatcherLocation {
      let matcher: RouteRecordMatcher | undefined
      let params: PathParams = {}
      let path: MatcherLocation['path']
      let name: MatcherLocation['name']
    
      if ('name' in location && location.name) {
        matcher = matcherMap.get(location.name)
    
        if(! matcher)throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
            location,
          })
    
        name = matcher.record.name
        params = assign(
          paramsFromLocation(
            currentLocation.params,
            matcher.keys.filter(k= >! k.optional).map(k= > k.name)
          ),
          location.params
        )
        path = matcher.stringify(params)
      } else if ('path' in location) {
        path = location.path
    
        matcher = matchers.find(m= > m.re.test(path))
        if (matcher) {
          params = matcher.parse(path)!
          name = matcher.record.name
        }
      } else {
        matcher = currentLocation.name
          ? matcherMap.get(currentLocation.name)
          : matchers.find(m= > m.re.test(currentLocation.path))
        if(! matcher)throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
            location,
            currentLocation,
          })
        name = matcher.record.name
        params = assign({}, currentLocation.params, location.params)
        path = matcher.stringify(params)
      }
    
      const matched: MatcherLocation['matched'] = []
      let parentMatcher: RouteRecordMatcher | undefined = matcher
      while (parentMatcher) {
        matched.unshift(parentMatcher.record)
        parentMatcher = parentMatcher.parent
      }
    
      return {
        name,
        path,
        params,
        matched,
        meta: mergeMetaFields(matched),
      }
    }
    Copy the code
  • The removeRoute method takes a parameter matcherRef. The parameter type can be passed in either the route name attribute or the matcher object, and the corresponding matcher or matcher index can be found through the matcherRef. Delete the corresponding matcher in matcherMap, matchers, and recursively delete the reference to the matcher object in matcher.children and matcher.alias.

    function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
      if (isRouteName(matcherRef)) {
        const matcher = matcherMap.get(matcherRef)
        if (matcher) {
          matcherMap.delete(matcherRef)
          matchers.splice(matchers.indexOf(matcher), 1)
          matcher.children.forEach(removeRoute)
          matcher.alias.forEach(removeRoute)
        }
      } else {
        let index = matchers.indexOf(matcherRef)
        if (index > -1) {
          matchers.splice(index, 1)
          if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
          matcherRef.children.forEach(removeRoute)
          matcherRef.alias.forEach(removeRoute)
        }
      }
    }
    Copy the code
  • The getRoutes method returns an array of matchers directly.

    function getRoutes() {
        return matchers
    }
    Copy the code
  • The getRecordMatcher method is a simple way to get the corresponding matcher object from the matcherMap by route name.

    function getRecordMatcher(name: RouteRecordName) {
        return matcherMap.get(name)
    }
    Copy the code

3. Install instance VueRouter

Through the above analysis, we know that:

  • throughcreateRouter(options)The VueRouter method creates the VueRouter object
  • The method takes a configuration object that must provide two properties: history and routes
    • History allows you to create different types of History objects as needed using the three methods provided by VueRouter, which provide route properties and route jump methods.
    • VueRouter creates a matcher object based on the Routes configuration. With the matcher object, VueRouter provides attributes and methods related to route configuration, such as adding routes, matching routes, and removing routes.

Let’s move on to how the VueRouter instance is associated with the Vue instance once the VueRouter instance object is created.

In the document sample code, add the VueRouter instance object to the Vue instance object via app.use.

Create and mount the root instance
const app = Vue.createApp({})
// Ensure that the _use_ routing instance enables the entire application to support routing.
app.use(router)
Copy the code

When app.use(router) executes, it actually calls the install method of the VueRouter instance.

export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = {
  path: '/'.name: undefined.params: {},
  query: {},
  hash: ' '.fullPath: '/'.matched: [].meta: {},
  redirectedFrom: undefined,}export function createRouter(options: RouterOptions) :Router {
  // shallowRef: Creates a ref that tracks its own.value changes but does not make its value responsive.
  const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
    START_LOCATION_NORMALIZED
  )

  let routerHistory = options.history

  const router: Router = {

    install(app: App) {
      const router = this
  
      // In the vUE instance, register the global routing components RouterLink and RouterView
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
  
      // Assign config.globalProperties.$router to the current VueRouter instance in the vue instance
      app.config.globalProperties.$router = router
      / * * * when reading app. Config. GlobalProperties. $route, * return unref (currentRoute), namely the current routing information, the initial value for the path for ` / ` object * /
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true.get: () = > unref(currentRoute),
      })

      if (
        isBrowser &&
        // Avoid multiple pushes when using the router in multiple applications. This is false only if it is stated at first install! started && currentRoute.value === START_LOCATION_NORMALIZED ) { started =true
        // Jump to the corresponding route in the browser URL
        push(routerHistory.location)
      }
      
      // Copy the currentRoute object and convert it to the reactive object reactiveRoute, which can be retrieved in the component via Inject routeLocationKey
      const reactiveRoute = {} as {
        [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
          RouteLocationNormalizedLoaded[k]
        >
      }
      for (let key in START_LOCATION_NORMALIZED) {
        // @ts-ignore: the key matches
        reactiveRoute[key] = computed(() = > currentRoute.value[key])
      }
      
      // Inject router providers into the vue instance. The component uses Inject to receive these values
      SRC /injectionSymbols.ts; // This Symbol is a Symbol
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      app.provide(routerViewLocationKey, currentRoute)
  
      // Intercepts the vue instance unmount method, resets some properties and events unbind when the vue instance is unmounted, and then executes the vue instance unmount method
      let unmountApp = app.unmount
      installedApps.add(app)
      app.unmount = function () {
        installedApps.delete(app)
        if (installedApps.size < 1) {
          removeHistoryListener()
          currentRoute.value = START_LOCATION_NORMALIZED
          started = false
          ready = false
        }
        unmountApp()
      }
    },
  }
}
Copy the code

In summary, the install method does a few things:

  • Register two routing components as VUE global components.
  • inapp.config.globalPropertiesTo add$routerand$routeProperties,$routerThe VueRouter instance object itself,$routeIs the routing object corresponding to the current location.
  • If it is the first time to install, it passespushMethod to jump to the route corresponding to the URL.
  • Inject three VueRouter related providers.
  • Intercepting vUE instancesunmountMethods,unmountThe VueRouter related uninstall is performed before the method is called.

Four,

Typescript makes it relatively easy to read source code, and provides a good understanding of what each variable does through type definitions. Starting from the creation of VueRouter instance object, we have a certain understanding of the basic implementation principle of routing, but also realize that it is not a very simple thing to achieve a complete routing function, and need to consider a lot of boundary problems. Due to my limited ability, THERE are still many details I cannot understand. Reading the source code is always a good thing, no matter how much you can understand.

This paper analyzes the basic part of VueRouter, and will continue to analyze the advanced part. Let’s learn and make progress together!

other

  • Document generation tool: Vitepress
  • Testing: Unit tests using JEST, E2E tests using Nightwatch
  • Changelog: Conventional – Changelog – CLI
  • Commit code checks: Lint-staged
  • Typescript: YYDS
  • There’s still a lot to fill in, like some TODO

History API browser compatibility

Most browsers now support the History API, and the Push, replace, and Go methods in VueRouter are all based on the History API.

My ability is limited, there may be some understanding of the mistake, welcome to pass by every big man give advice, thank you! 🙏