123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- /* @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, handleRouteEntered } 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
- }
- const prev = this.current
- this.confirmTransition(
- route,
- () => {
- 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) {
- // Initial redirection should not mark the history as ready yet
- // because it's triggered by the redirection instead
- // https://github.com/vuejs/vue-router/issues/3225
- // https://github.com/vuejs/vue-router/issues/3331
- if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
- this.ready = true
- this.readyErrorCbs.forEach(cb => {
- cb(err)
- })
- }
- }
- }
- )
- }
- confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
- const current = this.current
- this.pending = route
- 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)
- )
- 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, () => {
- // wait until async components are resolved before
- // extracting in-component enter guards
- const enterGuards = extractEnterGuards(activated)
- 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(() => {
- handleRouteEntered(route)
- })
- }
- })
- })
- }
- updateRoute (route: Route) {
- this.current = route
- this.cb && this.cb(route)
- }
- setupListeners () {
- // Default implementation is empty
- }
- teardown () {
- // clean up event listeners
- // https://github.com/vuejs/vue-router/issues/2341
- this.listeners.forEach(cleanupListener => {
- cleanupListener()
- })
- this.listeners = []
- // reset current history route
- // https://github.com/vuejs/vue-router/issues/3294
- this.current = START
- this.pending = null
- }
- }
- 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>
- ): Array<?Function> {
- return extractGuards(
- activated,
- 'beforeRouteEnter',
- (guard, _, match, key) => {
- return bindEnterGuard(guard, match, key)
- }
- )
- }
- function bindEnterGuard (
- guard: NavigationGuard,
- match: RouteRecord,
- key: string
- ): NavigationGuard {
- return function routeEnterGuard (to, from, next) {
- return guard(to, from, cb => {
- if (typeof cb === 'function') {
- if (!match.enteredCbs[key]) {
- match.enteredCbs[key] = []
- }
- match.enteredCbs[key].push(cb)
- }
- next(cb)
- })
- }
- }
|