diff --git a/flow/declarations.js b/flow/declarations.js index 824c3eca0..8377f789f 100644 --- a/flow/declarations.js +++ b/flow/declarations.js @@ -77,6 +77,7 @@ declare type RouteRecord = { beforeEnter: ?NavigationGuard; meta: any; props: boolean | Object | Function | Dictionary; + pathToRegexpOptions: PathToRegexpOptions; } declare type Location = { diff --git a/src/create-matcher.js b/src/create-matcher.js index f7d72b93e..306294e7b 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -22,14 +22,18 @@ export function createMatcher ( ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) + let _optimizedMatcher = optimizedMatcher(pathList, pathMap) + function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) + _optimizedMatcher = optimizedMatcher(pathList, pathMap) } function addRoute (parentOrRoute, route) { const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined // $flow-disable-line createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent) + _optimizedMatcher = optimizedMatcher(pathList, pathMap) // add aliases of parent if (parent && parent.alias.length) { @@ -82,12 +86,9 @@ export function createMatcher ( return _createRoute(record, location, redirectedFrom) } else if (location.path) { location.params = {} - for (let i = 0; i < pathList.length; i++) { - const path = pathList[i] - const record = pathMap[path] - if (matchRoute(record.regex, location.path, location.params)) { - return _createRoute(record, location, redirectedFrom) - } + const record = _optimizedMatcher(location.path, location.params) + if (record) { + return _createRoute(record, location, redirectedFrom) } } // no match @@ -224,3 +225,82 @@ function matchRoute ( function resolveRecordPath (path: string, record: RouteRecord): string { return resolvePath(path, record.parent ? record.parent.path : '/', true) } + +function isStatic (route) { + return ( + // Custom regex options are dynamic. + Object.keys(route.pathToRegexpOptions).length === 0 && + // Dynamic paths have /:placeholder or anonymous placeholder /(.+) or wildcard *. + !/[:(*]/.exec(route.path) + ) +} + +function firstLevelStatic (path) { + // firstLevel = ['/p/', '/p', '/' index: 0, input: '/p/b/c', groups: undefined] + const firstLevel = /^(\/[^:(*/]+)(\/|$)/.exec(path) + return firstLevel && firstLevel[1] +} + +function optimizedMatcher ( + pathList: Array, + pathMap: Dictionary +) { + // Lookup table + // e.g. Route /p/b is mapped as {"/p/b": route} + const staticMap = {} + // Buckets with the first level static path. + // e.g. Route /p/b/:id is mapped as {"/p": [route]} + const firstLevelStaticMap = {} + // Array of everything else. + const dynamic = [] + + pathList.forEach((path, index) => { + const route = pathMap[path] + const normalizedPath = route.regex.ignoreCase ? path.toLowerCase() : path + if (isStatic(route)) { + staticMap[normalizedPath] = { index, route } + } else { + const firstLevel = firstLevelStatic(path.toLowerCase()) + if (firstLevel) { + if (!firstLevelStaticMap[firstLevel]) { + firstLevelStaticMap[firstLevel] = [] + } + firstLevelStaticMap[firstLevel].push({ index, route }) + } else { + dynamic.push({ index, route }) + } + } + }) + + function match (path, params) { + const cleanPath = path.replace(/\/$/, '').toLowerCase() + let record = staticMap[cleanPath] + + const firstLevel = firstLevelStatic(path) + const firstLevelRoutes = firstLevelStaticMap[firstLevel] || [] + + for (let i = 0; i < firstLevelRoutes.length; i++) { + const { index, route } = firstLevelRoutes[i] + if (record && index >= record.index) { + break + } + if (matchRoute(route.regex, path, params)) { + record = firstLevelRoutes[i] + break + } + } + + for (let j = 0; j < dynamic.length; j++) { + const { index, route } = dynamic[j] + if (record && index >= record.index) { + break + } + if (matchRoute(route.regex, path, params)) { + record = dynamic[j] + } + } + return record && record.route + } + + return match +} diff --git a/src/create-route-map.js b/src/create-route-map.js index 93a558f8a..61affa3f1 100644 --- a/src/create-route-map.js +++ b/src/create-route-map.js @@ -92,6 +92,7 @@ function addRouteRecord ( const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), + pathToRegexpOptions, components: route.components || { default: route.component }, alias: route.alias ? typeof route.alias === 'string' diff --git a/test/unit/specs/create-matcher.spec.js b/test/unit/specs/create-matcher.spec.js index fcc2587c2..97da43b51 100644 --- a/test/unit/specs/create-matcher.spec.js +++ b/test/unit/specs/create-matcher.spec.js @@ -151,4 +151,163 @@ describe('Creating Matcher', function () { expect(pathForErrorRoute).toEqual('/error/') expect(pathForNotFoundRoute).toEqual('/') }) + + it('respect ordering', function () { + const matcher = createMatcher([ + { + path: '/p/staticbefore', + name: 'staticbefore', + component: { name: 'staticbefore' } + }, + { + path: '/p/:id', + name: 'dynamic', + component: { name: 'dynamic' } + }, + { + path: '/p/staticafter', + name: 'staticafter', + component: { name: 'staticafter' } + } + ]) + expect(matcher.match('/p/staticbefore').name).toBe('staticbefore') + expect(matcher.match('/p/staticafter').name).toBe('dynamic') + }) + + it('respect ordering for full dynamic', function () { + const matcher = createMatcher([ + { + path: '/before/static', + name: 'staticbefore', + component: { name: 'staticbefore' } + }, + { + path: '/:foo/static', + name: 'dynamic', + component: { name: 'dynamic' } + }, + { + path: '/after/static', + name: 'staticafter', + component: { name: 'staticafter' } + } + ]) + expect(matcher.match('/before/static').name).toBe('staticbefore') + expect(matcher.match('/after/static').name).toBe('dynamic') + }) + + it('respect ordering between full dynamic and first level static', function () { + const matcher = createMatcher([ + { + path: '/before/:foo', + name: 'staticbefore', + component: { name: 'staticbefore' } + }, + { + path: '/:foo/static', + name: 'dynamic', + component: { name: 'dynamic' } + }, + { + path: '/after/:foo', + name: 'staticafter', + component: { name: 'staticafter' } + } + ]) + expect(matcher.match('/before/static').name).toBe('staticbefore') + expect(matcher.match('/after/static').name).toBe('dynamic') + }) + + it('static can use sensitive flag', function () { + const matcher = createMatcher([ + { + path: '/p/sensitive', + name: 'sensitive', + pathToRegexpOptions: { + sensitive: true + }, + component: { name: 'sensitive' } + }, + { + path: '/p/insensitive', + name: 'insensitive', + component: { name: 'insensitive' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/SENSITIVE').name).toBe('not-found') + expect(matcher.match('/p/INSENSITIVE').name).toBe('insensitive') + }) + + it('static can use strict flag', function () { + const matcher = createMatcher([ + { + path: '/p/strict', + name: 'strict', + pathToRegexpOptions: { + strict: true + }, + component: { name: 'strict' } + }, + { + path: '/p/unstrict', + name: 'unstrict', + component: { name: 'unstrict' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/strict/').name).toBe('not-found') + expect(matcher.match('/p/unstrict/').name).toBe('unstrict') + }) + + it('static can use end flag', function () { + const matcher = createMatcher([ + { + path: '/p/end', + name: 'end', + component: { name: 'end' } + }, + { + path: '/p/not-end', + name: 'not-end', + pathToRegexpOptions: { + end: false + }, + component: { name: 'not-end' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/end/foo').name).toBe('not-found') + expect(matcher.match('/p/not-end/foo').name).toBe('not-end') + }) + + it('first level dynamic must work', function () { + const matcher = createMatcher([ + { + path: '/:foo/b', + name: 'b', + component: { name: 'b' } + }, + { + path: '/p/c', + name: 'c', + component: { name: 'c' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/b').name).toBe('b') + expect(matcher.match('/p/c').name).toBe('c') + }) }) diff --git a/test/unit/specs/error-handling.spec.js b/test/unit/specs/error-handling.spec.js index 52d61e210..a9b8e11e8 100644 --- a/test/unit/specs/error-handling.spec.js +++ b/test/unit/specs/error-handling.spec.js @@ -5,6 +5,10 @@ import { NavigationFailureType } from '../../../src/util/errors' Vue.use(VueRouter) describe('error handling', () => { + beforeEach(function () { + process.env.NODE_ENV = 'development' + }) + it('onReady errors', done => { const router = new VueRouter() const err = new Error('foo') diff --git a/types/router.d.ts b/types/router.d.ts index f8588f42f..4c5b63f3f 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -147,6 +147,7 @@ export type RouteConfig = RouteConfigSingleView | RouteConfigMultipleViews export interface RouteRecord { path: string regex: RegExp + pathToRegexpOptions: PathToRegexpOptions components: Dictionary instances: Dictionary name?: string