diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 816dd2c51..7fbf605b4 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -1,7 +1,7 @@ export { createWebHistory } from './history/html5' export { createMemoryHistory } from './history/memory' export { createWebHashHistory } from './history/hash' -export { createRouterMatcher } from './matcher' +export { createRouterMatcher, createRouterMatcherSortCache } from './matcher' export type { RouterMatcher } from './matcher' export { parseQuery, stringifyQuery } from './query' diff --git a/packages/router/src/matcher/index.ts b/packages/router/src/matcher/index.ts index 39a3c24b1..0ffd376f2 100644 --- a/packages/router/src/matcher/index.ts +++ b/packages/router/src/matcher/index.ts @@ -7,8 +7,12 @@ import { _RouteRecordProps, } from '../types' import { createRouterError, ErrorTypes, MatcherError } from '../errors' -import { createRouteRecordMatcher, RouteRecordMatcher } from './pathMatcher' -import { RouteRecordNormalized } from './types' +import { + createRouteRecordCacheKey, + createRouteRecordMatcher, + RouteRecordMatcher, +} from './pathMatcher' +import { RouteRecord, RouteRecordNormalized } from './types' import type { PathParams, @@ -61,8 +65,15 @@ export function createRouterMatcher( // normalized ordered array of matchers const matchers: RouteRecordMatcher[] = [] const matcherMap = new Map() + let restoreSort = globalOptions.sortCache != null + globalOptions = mergeOptions( - { strict: false, end: true, sensitive: false } as PathParserOptions, + { + strict: false, + end: true, + sensitive: false, + sortCache: undefined, + } as PathParserOptions, globalOptions ) @@ -223,17 +234,21 @@ export function createRouterMatcher( } function insertMatcher(matcher: RouteRecordMatcher) { - let i = 0 - while ( - i < matchers.length && - comparePathParserScore(matcher, matchers[i]) >= 0 && - // Adding children with empty path should still appear before the parent - // https://github.com/vuejs/router/issues/1124 - (matcher.record.path !== matchers[i].record.path || - !isRecordChildOf(matcher, matchers[i])) - ) - i++ - matchers.splice(i, 0, matcher) + if (restoreSort) { + matchers.push(matcher) + } else { + let i = 0 + while ( + i < matchers.length && + comparePathParserScore(matcher, matchers[i]) >= 0 && + // Adding children with empty path should still appear before the parent + // https://github.com/vuejs/router/issues/1124 + (matcher.record.path !== matchers[i].record.path || + !isRecordChildOf(matcher, matchers[i])) + ) + i++ + matchers.splice(i, 0, matcher) + } // only add the original record to the name map if (matcher.record.name && !isAliasRecord(matcher)) matcherMap.set(matcher.record.name, matcher) @@ -348,9 +363,45 @@ export function createRouterMatcher( // add initial routes routes.forEach(route => addRoute(route)) + // restore matcher order from cache + if (globalOptions.sortCache != null) { + try { + matchers.sort( + (a, b) => + globalOptions!.sortCache![a.cacheKey!] - + globalOptions!.sortCache![b.cacheKey!] + ) + + for (const entry of matchers) { + delete entry.cacheKey + } + } catch { + console.log('Restore failure.') + } + } + + restoreSort = false + return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher } } +/** + * Creates a router matcher sort cache + * + * @internal + */ +export function createRouterMatcherSortCache( + records: RouteRecord[] +): Record { + const sortCache: Record = {} + + for (let i = 0; i < records.length; i++) { + sortCache[createRouteRecordCacheKey(records[i])] = i + } + + return sortCache +} + function paramsFromLocation( params: MatcherLocation['params'], keys: string[] diff --git a/packages/router/src/matcher/pathMatcher.ts b/packages/router/src/matcher/pathMatcher.ts index aae2b7826..1573e6e9c 100644 --- a/packages/router/src/matcher/pathMatcher.ts +++ b/packages/router/src/matcher/pathMatcher.ts @@ -14,6 +14,12 @@ export interface RouteRecordMatcher extends PathParser { children: RouteRecordMatcher[] // aliases that must be removed when removing this record alias: RouteRecordMatcher[] + // defined when sort cache is present + cacheKey?: string +} + +export function createRouteRecordCacheKey(record: Readonly) { + return String(record?.name) + '___' + record.path } export function createRouteRecordMatcher( @@ -43,6 +49,10 @@ export function createRouteRecordMatcher( alias: [], }) + if (options?.sortCache) { + matcher.cacheKey = createRouteRecordCacheKey(record) + } + if (parent) { // both are aliases or both are not aliases // we don't want to mix them because the order is used when diff --git a/packages/router/src/matcher/pathParserRanker.ts b/packages/router/src/matcher/pathParserRanker.ts index 670013794..42946bc23 100644 --- a/packages/router/src/matcher/pathParserRanker.ts +++ b/packages/router/src/matcher/pathParserRanker.ts @@ -79,17 +79,28 @@ export interface _PathParserOptions { * @defaultValue `true` */ end?: boolean + + /** + * A precomputed object that maps matchers to its sorted index + * + * @internal + * @defaultValue `undefined` + */ + sortCache?: Record } export type PathParserOptions = Pick< _PathParserOptions, - 'end' | 'sensitive' | 'strict' + 'end' | 'sensitive' | 'strict' | 'sortCache' > // default pattern for a param: non-greedy everything but / const BASE_PARAM_PATTERN = '[^/]+?' -const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = { +const BASE_PATH_PARSER_OPTIONS: Omit< + Required<_PathParserOptions>, + 'sortCache' +> = { sensitive: false, strict: false, start: true,