Skip to content

Commit 199a98e

Browse files
committed
better error handling: second cb for router.onReady, implement router.onError
1 parent 9bfc50a commit 199a98e

File tree

6 files changed

+143
-37
lines changed

6 files changed

+143
-37
lines changed

src/components/view.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ function resolveProps (route, config) {
7777
case 'boolean':
7878
return config ? route.params : undefined
7979
default:
80-
warn(false, `props in "${route.path}" is a ${typeof config}, expecting an object, function or boolean.`)
80+
if (process.env.NODE_ENV !== 'production') {
81+
warn(
82+
false,
83+
`props in "${route.path}" is a ${typeof config}, ` +
84+
`expecting an object, function or boolean.`
85+
)
86+
}
8187
}
8288
}

src/create-matcher.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ export function createMatcher (routes: Array<RouteConfig>): Matcher {
7878
}
7979

8080
if (!redirect || typeof redirect !== 'object') {
81-
process.env.NODE_ENV !== 'production' && warn(
82-
false, `invalid redirect option: ${JSON.stringify(redirect)}`
83-
)
81+
if (process.env.NODE_ENV !== 'production') {
82+
warn(
83+
false, `invalid redirect option: ${JSON.stringify(redirect)}`
84+
)
85+
}
8486
return _createRoute(null, location)
8587
}
8688

@@ -117,7 +119,9 @@ export function createMatcher (routes: Array<RouteConfig>): Matcher {
117119
hash
118120
}, undefined, location)
119121
} else {
120-
warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
122+
if (process.env.NODE_ENV !== 'production') {
123+
warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
124+
}
121125
return _createRoute(null, location)
122126
}
123127
}

src/history/base.js

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export class History {
1515
cb: (r: Route) => void;
1616
ready: boolean;
1717
readyCbs: Array<Function>;
18+
readyErrorCbs: Array<Function>;
19+
errorCbs: Array<Function>;
1820

1921
// implemented by sub-classes
2022
+go: (n: number) => void;
@@ -31,20 +33,29 @@ export class History {
3133
this.pending = null
3234
this.ready = false
3335
this.readyCbs = []
36+
this.readyErrorCbs = []
37+
this.errorCbs = []
3438
}
3539

3640
listen (cb: Function) {
3741
this.cb = cb
3842
}
3943

40-
onReady (cb: Function) {
44+
onReady (cb: Function, errorCb: ?Function) {
4145
if (this.ready) {
4246
cb()
4347
} else {
4448
this.readyCbs.push(cb)
49+
if (errorCb) {
50+
this.readyErrorCbs.push(errorCb)
51+
}
4552
}
4653
}
4754

55+
onError (errorCb: Function) {
56+
this.errorCbs.push(errorCb)
57+
}
58+
4859
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
4960
const route = this.router.match(location, this.current)
5061
this.confirmTransition(route, () => {
@@ -55,16 +66,27 @@ export class History {
5566
// fire ready cbs once
5667
if (!this.ready) {
5768
this.ready = true
58-
this.readyCbs.forEach(cb => {
59-
cb(route)
60-
})
69+
this.readyCbs.forEach(cb => { cb(route) })
6170
}
62-
}, onAbort)
71+
}, err => {
72+
if (onAbort) {
73+
onAbort(err)
74+
}
75+
if (err && !this.ready) {
76+
this.ready = true
77+
this.readyErrorCbs.forEach(cb => { cb(err) })
78+
}
79+
})
6380
}
6481

6582
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
6683
const current = this.current
67-
const abort = () => { onAbort && onAbort() }
84+
const abort = err => {
85+
if (err instanceof Error) {
86+
this.errorCbs.forEach(cb => { cb(err) })
87+
}
88+
onAbort && onAbort(err)
89+
}
6890
if (
6991
isSameRoute(route, current) &&
7092
// in the case the route map has been dynamically appended to
@@ -98,20 +120,28 @@ export class History {
98120
if (this.pending !== route) {
99121
return abort()
100122
}
101-
hook(route, current, (to: any) => {
102-
if (to === false) {
103-
// next(false) -> abort navigation, ensure current URL
104-
this.ensureURL(true)
105-
abort()
106-
} else if (typeof to === 'string' || typeof to === 'object') {
107-
// next('/') or next({ path: '/' }) -> redirect
108-
(typeof to === 'object' && to.replace) ? this.replace(to) : this.push(to)
109-
abort()
110-
} else {
111-
// confirm transition and pass on the value
112-
next(to)
113-
}
114-
})
123+
try {
124+
hook(route, current, (to: any) => {
125+
if (to === false || to instanceof Error) {
126+
// next(false) -> abort navigation, ensure current URL
127+
this.ensureURL(true)
128+
abort(to)
129+
} else if (typeof to === 'string' || typeof to === 'object') {
130+
// next('/') or next({ path: '/' }) -> redirect
131+
abort()
132+
if (typeof to === 'object' && to.replace) {
133+
this.replace(to)
134+
} else {
135+
this.push(to)
136+
}
137+
} else {
138+
// confirm transition and pass on the value
139+
next(to)
140+
}
141+
})
142+
} catch (e) {
143+
abort(e)
144+
}
115145
}
116146

117147
runQueue(queue, iterator, () => {
@@ -128,7 +158,7 @@ export class History {
128158
onComplete(route)
129159
if (this.router.app) {
130160
this.router.app.$nextTick(() => {
131-
postEnterCbs.forEach(cb => cb())
161+
postEnterCbs.forEach(cb => { cb() })
132162
})
133163
}
134164
})
@@ -279,7 +309,7 @@ function poll (
279309
function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
280310
let _next
281311
let pending = 0
282-
let rejected = false
312+
let error = null
283313

284314
flatMapComponents(matched, (def, _, match, key) => {
285315
// if it's a function and doesn't have cid attached,
@@ -299,23 +329,31 @@ function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
299329
})
300330

301331
const reject = once(reason => {
302-
warn(false, `Failed to resolve async component ${key}: ${reason}`)
303-
if (!rejected) {
304-
rejected = true
305-
if (_next) _next(false)
332+
const msg = `Failed to resolve async component ${key}: ${reason}`
333+
process.env.NODE_ENV !== 'production' && warn(false, msg)
334+
if (!error) {
335+
error = reason instanceof Error
336+
? reason
337+
: new Error(msg)
338+
if (_next) _next(error)
306339
}
307340
})
308341

309-
const res = def(resolve, reject)
342+
let res
343+
try {
344+
res = def(resolve, reject)
345+
} catch (e) {
346+
reject(e)
347+
}
310348
if (res && typeof res.then === 'function') {
311349
res.then(resolve, reject)
312350
}
313351
}
314352
})
315353

316354
return (to, from, next) => {
317-
if (rejected) {
318-
next(false)
355+
if (error) {
356+
next(error)
319357
} else if (pending <= 0) {
320358
next()
321359
} else {

src/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,12 @@ export default class VueRouter {
124124
this.afterHooks.push(fn)
125125
}
126126

127-
onReady (cb: Function) {
128-
this.history.onReady(cb)
127+
onReady (cb: Function, errorCb?: Function) {
128+
this.history.onReady(cb, errorCb)
129+
}
130+
131+
onError (errorCb: Function) {
132+
this.history.onError(errorCb)
129133
}
130134

131135
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {

src/util/warn.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function assert (condition: any, message: string) {
77
}
88

99
export function warn (condition: any, message: string) {
10-
if (!condition) {
10+
if (process.env.NODE_ENV !== 'production' && !condition) {
1111
typeof console !== 'undefined' && console.warn(`[vue-router] ${message}`)
1212
}
1313
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import Vue from 'vue'
2+
import VueRouter from '../../../src/index'
3+
4+
Vue.use(VueRouter)
5+
6+
describe('error handling', () => {
7+
it('onReady errors', () => {
8+
const router = new VueRouter()
9+
const err = new Error('foo')
10+
router.beforeEach(() => { throw err })
11+
12+
const onReady = jasmine.createSpy('ready')
13+
const onError = jasmine.createSpy('error')
14+
router.onReady(onReady, onError)
15+
16+
router.push('/')
17+
18+
expect(onReady).not.toHaveBeenCalled()
19+
expect(onError).toHaveBeenCalledWith(err)
20+
})
21+
22+
it('navigation errors', () => {
23+
const router = new VueRouter()
24+
const err = new Error('foo')
25+
const spy = jasmine.createSpy('error')
26+
router.onError(spy)
27+
28+
router.push('/')
29+
router.beforeEach(() => { throw err })
30+
31+
router.push('/foo')
32+
expect(spy).toHaveBeenCalledWith(err)
33+
})
34+
35+
it('async component errors', () => {
36+
const err = new Error('foo')
37+
const spy1 = jasmine.createSpy('error')
38+
const spy2 = jasmine.createSpy('errpr')
39+
const Comp = () => { throw err }
40+
const router = new VueRouter({
41+
routes: [
42+
{ path: '/', component: Comp }
43+
]
44+
})
45+
46+
router.onError(spy1)
47+
router.onReady(() => {}, spy2)
48+
49+
router.push('/')
50+
51+
expect(spy1).toHaveBeenCalledWith(err)
52+
expect(spy2).toHaveBeenCalledWith(err)
53+
})
54+
})

0 commit comments

Comments
 (0)