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 approved
rollup
Build tools for packaging - Can be achieved by
rollup.config.js
File 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 is
RouterHistory
. - Routes: indicates the initial route list that should be added to the route. The type is
RouteRecordRaw
.
The following attributes are not required:
- ScrollBehavior: Function that controls scrolling while navigating between pages. You can return a
Promise
To delay scrolling. - ParseQuery: A custom implementation for parsing queries. The query key and value must be decoded. See corresponding
stringifyQuery
. - StringifyQuery: Custom implementation of stringing a query object. I shouldn’t have put? . The query key and value should be properly encoded.
parseQuery
Corresponds to processing query resolution. - LinkActiveClass: Default class for activating the RouterLink. If nothing is provided, will it be used
router-link-active
. - LinkExactActiveClass: Default class for precise activation of the RouterLink. If nothing is provided, will it be used
router-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: RouteRecord
Save the formatted route configuration recordsparent: RouteRecordMatcher | undefined
Save the parent route matcher objectchildren: RouteRecordMatcher[]
Child route, initialized to an empty arrayalias: 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:
- through
createRouter(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.
- in
app.config.globalProperties
To add$router
and$route
Properties,$router
The VueRouter instance object itself,$route
Is the routing object corresponding to the current location. - If it is the first time to install, it passes
push
Method to jump to the route corresponding to the URL. - Inject three VueRouter related providers.
- Intercepting vUE instances
unmount
Methods,unmount
The 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! 🙏