UNPKG

vue-router

Version:
404 lines (376 loc) 10.7 kB
/* @flow */ import { _Vue } from '../install' import type Router from '../index' import { inBrowser } from '../util/dom' import { runQueue } from '../util/async' import { warn } from '../util/warn' import { START, isSameRoute } from '../util/route' import { flatten, flatMapComponents, resolveAsyncComponents } from '../util/resolve-components' import { createNavigationDuplicatedError, createNavigationCancelledError, createNavigationRedirectedError, createNavigationAbortedError, isError, isNavigationFailure, NavigationFailureType } from '../util/errors' export class History { router: Router base: string current: Route pending: ?Route cb: (r: Route) => void ready: boolean readyCbs: Array<Function> readyErrorCbs: Array<Function> errorCbs: Array<Function> listeners: Array<Function> cleanupListeners: Function // implemented by sub-classes +go: (n: number) => void +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void +replace: ( loc: RawLocation, onComplete?: Function, onAbort?: Function ) => void +ensureURL: (push?: boolean) => void +getCurrentLocation: () => string +setupListeners: Function constructor (router: Router, base: ?string) { this.router = router this.base = normalizeBase(base) // start with a route object that stands for "nowhere" this.current = START this.pending = null this.ready = false this.readyCbs = [] this.readyErrorCbs = [] this.errorCbs = [] this.listeners = [] } listen (cb: Function) { this.cb = cb } onReady (cb: Function, errorCb: ?Function) { if (this.ready) { cb() } else { this.readyCbs.push(cb) if (errorCb) { this.readyErrorCbs.push(errorCb) } } } onError (errorCb: Function) { this.errorCbs.push(errorCb) } transitionTo ( location: RawLocation, onComplete?: Function, onAbort?: Function ) { let route // catch redirect option https://github.com/vuejs/vue-router/issues/3201 try { route = this.router.match(location, this.current) } catch (e) { this.errorCbs.forEach(cb => { cb(e) }) // Exception should still be thrown throw e } this.confirmTransition( route, () => { const prev = this.current this.updateRoute(route) onComplete && onComplete(route) this.ensureURL() this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) // fire ready cbs once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { if (onAbort) { onAbort(err) } if (err && !this.ready) { this.ready = true // Initial redirection should still trigger the onReady onSuccess // https://github.com/vuejs/vue-router/issues/3225 if (!isNavigationFailure(err, NavigationFailureType.redirected)) { this.readyErrorCbs.forEach(cb => { cb(err) }) } else { this.readyCbs.forEach(cb => { cb(route) }) } } } ) } confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current const abort = err => { // 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 (!isNavigationFailure(err) && isError(err)) { if (this.errorCbs.length) { this.errorCbs.forEach(cb => { cb(err) }) } else { warn(false, 'uncaught error during route navigation:') console.error(err) } } onAbort && onAbort(err) } const lastRouteIndex = route.matched.length - 1 const lastCurrentIndex = current.matched.length - 1 if ( isSameRoute(route, current) && // in the case the route map has been dynamically appended to lastRouteIndex === lastCurrentIndex && route.matched[lastRouteIndex] === current.matched[lastCurrentIndex] ) { this.ensureURL() return abort(createNavigationDuplicatedError(current, route)) } const { updated, deactivated, activated } = resolveQueue( this.current.matched, route.matched ) const queue: Array<?NavigationGuard> = [].concat( // in-component leave guards extractLeaveGuards(deactivated), // global before hooks this.router.beforeHooks, // in-component update hooks extractUpdateHooks(updated), // in-config enter guards activated.map(m => m.beforeEnter), // async components resolveAsyncComponents(activated) ) this.pending = route const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) { return abort(createNavigationCancelledError(current, route)) } try { hook(route, current, (to: any) => { if (to === false) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true) abort(createNavigationAbortedError(current, route)) } else if (isError(to)) { this.ensureURL(true) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string')) ) { // next('/') or next({ path: '/' }) -> redirect abort(createNavigationRedirectedError(current, route)) if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // confirm transition and pass on the value next(to) } }) } catch (e) { abort(e) } } runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // wait until async components are resolved before // extracting in-component enter guards const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { if (this.pending !== route) { return abort(createNavigationCancelledError(current, route)) } this.pending = null onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } }) }) } updateRoute (route: Route) { this.current = route this.cb && this.cb(route) } setupListeners () { // Default implementation is empty } teardownListeners () { this.listeners.forEach(cleanupListener => { cleanupListener() }) this.listeners = [] } } function normalizeBase (base: ?string): string { if (!base) { if (inBrowser) { // respect <base> tag const baseEl = document.querySelector('base') base = (baseEl && baseEl.getAttribute('href')) || '/' // strip full URL origin base = base.replace(/^https?:\/\/[^\/]+/, '') } else { base = '/' } } // make sure there's the starting slash if (base.charAt(0) !== '/') { base = '/' + base } // remove trailing slash return base.replace(/\/$/, '') } function resolveQueue ( current: Array<RouteRecord>, next: Array<RouteRecord> ): { updated: Array<RouteRecord>, activated: Array<RouteRecord>, deactivated: Array<RouteRecord> } { let i const max = Math.max(current.length, next.length) for (i = 0; i < max; i++) { if (current[i] !== next[i]) { break } } return { updated: next.slice(0, i), activated: next.slice(i), deactivated: current.slice(i) } } function extractGuards ( records: Array<RouteRecord>, name: string, bind: Function, reverse?: boolean ): Array<?Function> { const guards = flatMapComponents(records, (def, instance, match, key) => { const guard = extractGuard(def, name) if (guard) { return Array.isArray(guard) ? guard.map(guard => bind(guard, instance, match, key)) : bind(guard, instance, match, key) } }) return flatten(reverse ? guards.reverse() : guards) } function extractGuard ( def: Object | Function, key: string ): NavigationGuard | Array<NavigationGuard> { if (typeof def !== 'function') { // extend now so that global mixins are applied. def = _Vue.extend(def) } return def.options[key] } function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> { return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) } function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> { return extractGuards(updated, 'beforeRouteUpdate', bindGuard) } function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard { if (instance) { return function boundRouteGuard () { return guard.apply(instance, arguments) } } } function extractEnterGuards ( activated: Array<RouteRecord>, cbs: Array<Function>, isValid: () => boolean ): Array<?Function> { return extractGuards( activated, 'beforeRouteEnter', (guard, _, match, key) => { return bindEnterGuard(guard, match, key, cbs, isValid) } ) } function bindEnterGuard ( guard: NavigationGuard, match: RouteRecord, key: string, cbs: Array<Function>, isValid: () => boolean ): NavigationGuard { return function routeEnterGuard (to, from, next) { return guard(to, from, cb => { if (typeof cb === 'function') { cbs.push(() => { // #750 // if a router-view is wrapped with an out-in transition, // the instance may not have been registered at this time. // we will need to poll for registration until current route // is no longer valid. poll(cb, match.instances, key, isValid) }) } next(cb) }) } } function poll ( cb: any, // somehow flow cannot infer this is a function instances: Object, key: string, isValid: () => boolean ) { if ( instances[key] && !instances[key]._isBeingDestroyed // do not reuse being destroyed instance ) { cb(instances[key]) } else if (isValid()) { setTimeout(() => { poll(cb, instances, key, isValid) }, 16) } }