diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 636f2a240..365261eb6 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -26,23 +26,39 @@ module.exports = { description: 'Vue.js 공식 라우터' }, '/fr/': { - lang: 'fr', - title: 'Vue Router', - description: 'Routeur officiel pour Vue.Js' + lang: 'fr', + title: 'Vue Router', + description: 'Routeur officiel pour Vue.Js' } }, head: [ ['link', { rel: 'icon', href: `/logo.png` }], - ['link', { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png` }], - ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }], - ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }], + [ + 'link', + { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png` } + ], + [ + 'link', + { + rel: 'mask-icon', + href: '/icons/safari-pinned-tab.svg', + color: '#3eaf7c' + } + ], + [ + 'meta', + { + name: 'msapplication-TileImage', + content: '/icons/msapplication-icon-144x144.png' + } + ] ], serviceWorker: true, theme: 'vue', themeConfig: { algolia: { apiKey: 'f854bb46d3de7eeb921a3b9173bd0d4c', - indexName: 'vue-router', + indexName: 'vue-router' }, repo: 'vuejs/vue-router', docsDir: 'docs', @@ -92,7 +108,8 @@ module.exports = { '/guide/advanced/transitions.md', '/guide/advanced/data-fetching.md', '/guide/advanced/scroll-behavior.md', - '/guide/advanced/lazy-loading.md' + '/guide/advanced/lazy-loading.md', + '/guide/advanced/navigation-failures.md' ] } ] @@ -298,55 +315,55 @@ module.exports = { ] }, '/fr/': { - label: 'Français', - selectText: 'Langues', - editLinkText: 'Editer cette page sur Github', - nav: [ - { - text: 'Guide', - link: '/fr/guide/' - }, - { - text: 'API', - link: '/fr/api/' - }, - { - text: 'Notes de version', - link: 'https://github.com/vuejs/vue-router/releases' - } - ], - sidebar: [ - '/fr/installation.md', - '/fr/', - { - title: 'Essentiels', - collapsable: false, - children: [ - '/fr/guide/', - '/fr/guide/essentials/dynamic-matching.md', - '/fr/guide/essentials/nested-routes.md', - '/fr/guide/essentials/navigation.md', - '/fr/guide/essentials/named-routes.md', - '/fr/guide/essentials/named-views.md', - '/fr/guide/essentials/redirect-and-alias.md', - '/fr/guide/essentials/passing-props.md', - '/fr/guide/essentials/history-mode.md' - ] - }, - { - title: 'Avancés', - collapsable: false, - children: [ - '/fr/guide/advanced/navigation-guards.md', - '/fr/guide/advanced/meta.md', - '/fr/guide/advanced/transitions.md', - '/fr/guide/advanced/data-fetching.md', - '/fr/guide/advanced/scroll-behavior.md', - '/fr/guide/advanced/lazy-loading.md' - ] - } - ] - }, + label: 'Français', + selectText: 'Langues', + editLinkText: 'Editer cette page sur Github', + nav: [ + { + text: 'Guide', + link: '/fr/guide/' + }, + { + text: 'API', + link: '/fr/api/' + }, + { + text: 'Notes de version', + link: 'https://github.com/vuejs/vue-router/releases' + } + ], + sidebar: [ + '/fr/installation.md', + '/fr/', + { + title: 'Essentiels', + collapsable: false, + children: [ + '/fr/guide/', + '/fr/guide/essentials/dynamic-matching.md', + '/fr/guide/essentials/nested-routes.md', + '/fr/guide/essentials/navigation.md', + '/fr/guide/essentials/named-routes.md', + '/fr/guide/essentials/named-views.md', + '/fr/guide/essentials/redirect-and-alias.md', + '/fr/guide/essentials/passing-props.md', + '/fr/guide/essentials/history-mode.md' + ] + }, + { + title: 'Avancés', + collapsable: false, + children: [ + '/fr/guide/advanced/navigation-guards.md', + '/fr/guide/advanced/meta.md', + '/fr/guide/advanced/transitions.md', + '/fr/guide/advanced/data-fetching.md', + '/fr/guide/advanced/scroll-behavior.md', + '/fr/guide/advanced/lazy-loading.md' + ] + } + ] + } } } } diff --git a/docs/guide/advanced/navigation-failures.md b/docs/guide/advanced/navigation-failures.md new file mode 100644 index 000000000..d784a4aec --- /dev/null +++ b/docs/guide/advanced/navigation-failures.md @@ -0,0 +1,60 @@ +# Navigation Failures + +> New in 3.4.0 + +When using `router-link`, Vue Router calls `router.push` to trigger a navigation. While the expected behavior for most links is to navigate a user to a new page, there are a few situations where users will remain on the same page: + +- Users are already on the page that they are trying to navigate to +- A [navigation guard](./navigation-guards.md) aborts the navigation by calling `next(false)` +- A [navigation guard](./navigation-guards.md) throws an error or calls `next(new Error())` + +When using a `router-link` component, **none of these failures will log an error**. However, if you are using `router.push` or `router.replace`, you might come across an _"Uncaught (in promise) Error"_ message followed by a more specific message in your console. Let's understand how to differentiate _Navigation Failures_. + +::: tip Background story + In v3.2.0, _Navigation Failures_ were exposed through the two optional callbacks of `router.push`: `onComplete` and `onAbort`. Since version 3.1.0, `router.push` and `router.replace` return a _Promise_ if no `onComplete`/`onAbort` callback is provided. This _Promise_ resolves instead of invoking `onComplete` and rejects instead of invoking `onAbort`. + ::: + +## Detecting Navigation Failures + +_Navigation Failures_ are `Error` instances with a few extra properties. To check if an error comes from the Router, use the `isNavigationFailure` function: + +```js +import { NavigationFailureType, isNavigationFailure } from 'vue-router' + +// trying to access the admin page +router.push('/admin').catch(failure => { + if (isNavigationFailure(failure, NavigationFailureType.redirected)) { + // show a small notification to the user + showToast('Login in order to access the admin panel') + } +}) +``` + +::: tip +If you omit the second parameter: `isNavigationFailure(failure)`, it will only check if the error is a _Navigation Failure_. +::: + +## `NavigationFailureType` + +`NavigationFailureType` help developers to differentiate between the various types of _Navigation Failures_. There are four different types: + +- `redirected`: `next(newLocation)` was called inside of a navigation guard to redirect somewhere else. +- `aborted`: `next(false)` was called inside of a navigation guard to the navigation. +- `cancelled`: A new navigation completely took place before the current navigation could finish. e.g. `router.push` was called while waiting inside of a navigation guard. +- `duplicated`: The navigation was prevented because we are already at the target location. + +## _Navigation Failures_'s properties + +All navigation failures expose `to` and `from` properties to reflect the current location as well as the target location for the navigation that failed: + +```js +// trying to access the admin page +router.push('/admin').catch(failure => { + if (isNavigationFailure(failure, NavigationFailureType.redirected)) { + failure.to.path // '/admin' + failure.from.path // '/' + } +}) +``` + +In all cases, `to` and `from` are normalized route locations. diff --git a/src/history/abstract.js b/src/history/abstract.js index 1add43d01..325e4b954 100644 --- a/src/history/abstract.js +++ b/src/history/abstract.js @@ -2,8 +2,7 @@ import type Router from '../index' import { History } from './base' -import { isRouterError } from '../util/warn' -import { NavigationFailureType } from './errors' +import { NavigationFailureType, isNavigationFailure } from '../util/errors' export class AbstractHistory extends History { index: number @@ -51,7 +50,7 @@ export class AbstractHistory extends History { this.updateRoute(route) }, err => { - if (isRouterError(err, NavigationFailureType.duplicated)) { + if (isNavigationFailure(err, NavigationFailureType.duplicated)) { this.index = targetIndex } } diff --git a/src/history/base.js b/src/history/base.js index 7425df13a..072cfb8e8 100644 --- a/src/history/base.js +++ b/src/history/base.js @@ -4,7 +4,7 @@ import { _Vue } from '../install' import type Router from '../index' import { inBrowser } from '../util/dom' import { runQueue } from '../util/async' -import { warn, isError, isRouterError } from '../util/warn' +import { warn } from '../util/warn' import { START, isSameRoute } from '../util/route' import { flatten, @@ -16,8 +16,10 @@ import { createNavigationCancelledError, createNavigationRedirectedError, createNavigationAbortedError, + isError, + isNavigationFailure, NavigationFailureType -} from './errors' +} from '../util/errors' export class History { router: Router @@ -35,7 +37,11 @@ export class History { // implemented by sub-classes +go: (n: number) => void +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void - +replace: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void + +replace: ( + loc: RawLocation, + onComplete?: Function, + onAbort?: Function + ) => void +ensureURL: (push?: boolean) => void +getCurrentLocation: () => string +setupListeners: Function @@ -115,7 +121,7 @@ export class History { this.ready = true // Initial redirection should still trigger the onReady onSuccess // https://github.com/vuejs/vue-router/issues/3225 - if (!isRouterError(err, NavigationFailureType.redirected)) { + if (!isNavigationFailure(err, NavigationFailureType.redirected)) { this.readyErrorCbs.forEach(cb => { cb(err) }) @@ -135,7 +141,7 @@ export class History { // changed after adding errors with // https://github.com/vuejs/vue-router/pull/3047 before that change, // redirect and aborted navigation would produce an err == null - if (!isRouterError(err) && isError(err)) { + if (!isNavigationFailure(err) && isError(err)) { if (this.errorCbs.length) { this.errorCbs.forEach(cb => { cb(err) diff --git a/src/index.js b/src/index.js index ae3b69e15..22ecff96d 100644 --- a/src/index.js +++ b/src/index.js @@ -16,22 +16,26 @@ import { AbstractHistory } from './history/abstract' import type { Matcher } from './create-matcher' +import { isNavigationFailure, NavigationFailureType } from './util/errors' + export default class VueRouter { - static install: () => void; - static version: string; - - app: any; - apps: Array; - ready: boolean; - readyCbs: Array; - options: RouterOptions; - mode: string; - history: HashHistory | HTML5History | AbstractHistory; - matcher: Matcher; - fallback: boolean; - beforeHooks: Array; - resolveHooks: Array; - afterHooks: Array; + static install: () => void + static version: string + static isNavigationFailure: Function + static NavigationFailureType: any + + app: any + apps: Array + ready: boolean + readyCbs: Array + options: RouterOptions + mode: string + history: HashHistory | HTML5History | AbstractHistory + matcher: Matcher + fallback: boolean + beforeHooks: Array + resolveHooks: Array + afterHooks: Array constructor (options: RouterOptions = {}) { this.app = null @@ -43,7 +47,8 @@ export default class VueRouter { this.matcher = createMatcher(options.routes || [], this) let mode = options.mode || 'hash' - this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false + this.fallback = + mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } @@ -69,11 +74,7 @@ export default class VueRouter { } } - match ( - raw: RawLocation, - current?: Route, - redirectedFrom?: Location - ): Route { + match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route { return this.matcher.match(raw, current, redirectedFrom) } @@ -82,11 +83,12 @@ export default class VueRouter { } init (app: any /* Vue component instance */) { - process.env.NODE_ENV !== 'production' && assert( - install.installed, - `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + - `before creating root instance.` - ) + process.env.NODE_ENV !== 'production' && + assert( + install.installed, + `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + + `before creating root instance.` + ) this.apps.push(app) @@ -118,7 +120,7 @@ export default class VueRouter { const history = this.history if (history instanceof HTML5History || history instanceof HashHistory) { - const handleInitialScroll = (routeOrError) => { + const handleInitialScroll = routeOrError => { const from = history.current const expectScroll = this.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll @@ -127,15 +129,19 @@ export default class VueRouter { handleScroll(this, routeOrError, from, false) } } - const setupListeners = (routeOrError) => { + const setupListeners = routeOrError => { history.setupListeners() handleInitialScroll(routeOrError) } - history.transitionTo(history.getCurrentLocation(), setupListeners, setupListeners) + history.transitionTo( + history.getCurrentLocation(), + setupListeners, + setupListeners + ) } history.listen(route => { - this.apps.forEach((app) => { + this.apps.forEach(app => { app._route = route }) }) @@ -204,11 +210,14 @@ export default class VueRouter { if (!route) { return [] } - return [].concat.apply([], route.matched.map(m => { - return Object.keys(m.components).map(key => { - return m.components[key] + return [].concat.apply( + [], + route.matched.map(m => { + return Object.keys(m.components).map(key => { + return m.components[key] + }) }) - })) + ) } resolve ( @@ -224,12 +233,7 @@ export default class VueRouter { resolved: Route } { current = current || this.history.current - const location = normalizeLocation( - to, - current, - append, - this - ) + const location = normalizeLocation(to, current, append, this) const route = this.match(location, current) const fullPath = route.redirectedFrom || route.fullPath const base = this.history.base @@ -267,6 +271,8 @@ function createHref (base: string, fullPath: string, mode) { VueRouter.install = install VueRouter.version = '__VERSION__' +VueRouter.isNavigationFailure = isNavigationFailure +VueRouter.NavigationFailureType = NavigationFailureType if (inBrowser && window.Vue) { window.Vue.use(VueRouter) diff --git a/src/history/errors.js b/src/util/errors.js similarity index 75% rename from src/history/errors.js rename to src/util/errors.js index 0eaa5a8df..bacf6881e 100644 --- a/src/history/errors.js +++ b/src/util/errors.js @@ -1,8 +1,8 @@ export const NavigationFailureType = { - redirected: 1, - aborted: 2, - cancelled: 3, - duplicated: 4 + redirected: 2, + aborted: 4, + cancelled: 8, + duplicated: 16 } export function createNavigationRedirectedError (from, to) { @@ -17,12 +17,15 @@ export function createNavigationRedirectedError (from, to) { } export function createNavigationDuplicatedError (from, to) { - return createRouterError( + const error = createRouterError( from, to, NavigationFailureType.duplicated, `Avoided redundant navigation to current location: "${from.fullPath}".` ) + // backwards compatible with the first introduction of Errors + error.name = 'NavigationDuplicated' + return error } export function createNavigationCancelledError (from, to) { @@ -68,3 +71,15 @@ function stringifyRoute (to) { }) return JSON.stringify(location, null, 2) } + +export function isError (err) { + return Object.prototype.toString.call(err).indexOf('Error') > -1 +} + +export function isNavigationFailure (err, errorType) { + return ( + isError(err) && + err._isRouter && + (errorType == null || err.type === errorType) + ) +} diff --git a/src/util/resolve-components.js b/src/util/resolve-components.js index 3f7608cd5..204e3ca8f 100644 --- a/src/util/resolve-components.js +++ b/src/util/resolve-components.js @@ -1,7 +1,8 @@ /* @flow */ import { _Vue } from '../install' -import { warn, isError } from './warn' +import { warn } from './warn' +import { isError } from '../util/errors' export function resolveAsyncComponents (matched: Array): Function { return (to, from, next) => { diff --git a/src/util/warn.js b/src/util/warn.js index 73e70caf8..025d3b20f 100644 --- a/src/util/warn.js +++ b/src/util/warn.js @@ -12,10 +12,3 @@ export function warn (condition: any, message: string) { } } -export function isError (err: any): boolean { - return Object.prototype.toString.call(err).indexOf('Error') > -1 -} - -export function isRouterError (err: any, errorType: ?string): boolean { - return isError(err) && err._isRouter && (errorType == null || err.type === errorType) -} diff --git a/test/unit/specs/error-handling.spec.js b/test/unit/specs/error-handling.spec.js index 003dad6f9..2f0589f4d 100644 --- a/test/unit/specs/error-handling.spec.js +++ b/test/unit/specs/error-handling.spec.js @@ -1,6 +1,6 @@ import Vue from 'vue' import VueRouter from '../../../src/index' -import { NavigationFailureType } from '../../../src/history/errors' +import { NavigationFailureType } from '../../../src/util/errors' Vue.use(VueRouter) diff --git a/types/router.d.ts b/types/router.d.ts index de0e327fe..07f912eb6 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -61,6 +61,17 @@ export declare class VueRouter { } static install: PluginFunction + static version: string + + static isNavigationFailure: (error: any, type?: NavigationFailureTypeE) => error is Error + static NavigationFailureType: NavigationFailureTypeE +} + +export enum NavigationFailureTypeE { + redirected = 1, + aborted = 2, + cancelled = 3, + duplicated = 4 } type Position = { x: number; y: number }