const toggle = ref(true)
const root = nodeOps.createElement('div')
createApp({
- components: { Foo },
render: () => (toggle.value ? h(Foo) : null)
}).mount(root)
new Promise(r => {
resolve = r as any
}),
- loading: () => 'loading',
+ loadingComponent: () => 'loading',
delay: 1 // defaults to 200
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
createApp({
- components: { Foo },
render: () => (toggle.value ? h(Foo) : null)
}).mount(root)
new Promise(r => {
resolve = r as any
}),
- loading: () => 'loading',
+ loadingComponent: () => 'loading',
delay: 0
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
createApp({
- components: { Foo },
render: () => (toggle.value ? h(Foo) : null)
}).mount(root)
const toggle = ref(true)
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () => (toggle.value ? h(Foo) : null)
})
resolve = _resolve as any
reject = _reject
}),
- error: (props: { error: Error }) => props.error.message
+ errorComponent: (props: { error: Error }) => props.error.message
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () => (toggle.value ? h(Foo) : null)
})
resolve = _resolve as any
reject = _reject
}),
- error: (props: { error: Error }) => props.error.message,
- loading: () => 'loading',
+ errorComponent: (props: { error: Error }) => props.error.message,
+ loadingComponent: () => 'loading',
delay: 1
})
const toggle = ref(true)
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () => (toggle.value ? h(Foo) : null)
})
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () => h(Foo)
})
resolve = _resolve as any
}),
timeout: 1,
- error: () => 'timed out'
+ errorComponent: () => 'timed out'
})
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () => h(Foo)
})
}),
delay: 1,
timeout: 16,
- error: () => 'timed out',
- loading: () => 'loading'
+ errorComponent: () => 'timed out',
+ loadingComponent: () => 'loading'
})
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
}),
delay: 1,
timeout: 16,
- loading: () => 'loading'
+ loadingComponent: () => 'loading'
})
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () => h(Foo)
})
const handler = (app.config.errorHandler = jest.fn())
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
const root = nodeOps.createElement('div')
const app = createApp({
- components: { Foo },
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
expect(handler).toHaveBeenCalled()
expect(serializeInner(root)).toBe('<!----> & <!---->')
})
+
+ test('retry (success)', async () => {
+ let loaderCallCount = 0
+ let resolve: (comp: Component) => void
+ let reject: (e: Error) => void
+
+ const Foo = defineAsyncComponent({
+ loader: () => {
+ loaderCallCount++
+ return new Promise((_resolve, _reject) => {
+ resolve = _resolve as any
+ reject = _reject
+ })
+ },
+ retryWhen: error => error.message.match(/foo/)
+ })
+
+ const root = nodeOps.createElement('div')
+ const app = createApp({
+ render: () => h(Foo)
+ })
+
+ const handler = (app.config.errorHandler = jest.fn())
+ app.mount(root)
+ expect(serializeInner(root)).toBe('<!---->')
+ expect(loaderCallCount).toBe(1)
+
+ const err = new Error('foo')
+ reject!(err)
+ await timeout()
+ expect(handler).not.toHaveBeenCalled()
+ expect(loaderCallCount).toBe(2)
+ expect(serializeInner(root)).toBe('<!---->')
+
+ // should render this time
+ resolve!(() => 'resolved')
+ await timeout()
+ expect(handler).not.toHaveBeenCalled()
+ expect(serializeInner(root)).toBe('resolved')
+ })
+
+ test('retry (skipped)', async () => {
+ let loaderCallCount = 0
+ let reject: (e: Error) => void
+
+ const Foo = defineAsyncComponent({
+ loader: () => {
+ loaderCallCount++
+ return new Promise((_resolve, _reject) => {
+ reject = _reject
+ })
+ },
+ retryWhen: error => error.message.match(/bar/)
+ })
+
+ const root = nodeOps.createElement('div')
+ const app = createApp({
+ render: () => h(Foo)
+ })
+
+ const handler = (app.config.errorHandler = jest.fn())
+ app.mount(root)
+ expect(serializeInner(root)).toBe('<!---->')
+ expect(loaderCallCount).toBe(1)
+
+ const err = new Error('foo')
+ reject!(err)
+ await timeout()
+ // should fail because retryWhen returns false
+ expect(handler).toHaveBeenCalled()
+ expect(handler.mock.calls[0][0]).toBe(err)
+ expect(loaderCallCount).toBe(1)
+ expect(serializeInner(root)).toBe('<!---->')
+ })
+
+ test('retry (fail w/ maxRetries)', async () => {
+ let loaderCallCount = 0
+ let reject: (e: Error) => void
+
+ const Foo = defineAsyncComponent({
+ loader: () => {
+ loaderCallCount++
+ return new Promise((_resolve, _reject) => {
+ reject = _reject
+ })
+ },
+ retryWhen: error => error.message.match(/foo/),
+ maxRetries: 1
+ })
+
+ const root = nodeOps.createElement('div')
+ const app = createApp({
+ render: () => h(Foo)
+ })
+
+ const handler = (app.config.errorHandler = jest.fn())
+ app.mount(root)
+ expect(serializeInner(root)).toBe('<!---->')
+ expect(loaderCallCount).toBe(1)
+
+ // first retry
+ const err = new Error('foo')
+ reject!(err)
+ await timeout()
+ expect(handler).not.toHaveBeenCalled()
+ expect(loaderCallCount).toBe(2)
+ expect(serializeInner(root)).toBe('<!---->')
+
+ // 2nd retry, should fail due to reaching maxRetries
+ reject!(err)
+ await timeout()
+ expect(handler).toHaveBeenCalled()
+ expect(handler.mock.calls[0][0]).toBe(err)
+ expect(loaderCallCount).toBe(2)
+ expect(serializeInner(root)).toBe('<!---->')
+ })
})
ComponentInternalInstance,
isInSSRComponentSetup
} from './component'
-import { isFunction, isObject, EMPTY_OBJ } from '@vue/shared'
+import { isFunction, isObject, EMPTY_OBJ, NO } from '@vue/shared'
import { ComponentPublicInstance } from './componentProxy'
import { createVNode } from './vnode'
import { defineComponent } from './apiDefineComponent'
export interface AsyncComponentOptions<T = any> {
loader: AsyncComponentLoader<T>
- loading?: PublicAPIComponent
- error?: PublicAPIComponent
+ loadingComponent?: PublicAPIComponent
+ errorComponent?: PublicAPIComponent
delay?: number
timeout?: number
+ retryWhen?: (error: Error) => any
+ maxRetries?: number
suspensible?: boolean
}
}
const {
- suspensible = true,
loader,
- loading: loadingComponent,
- error: errorComponent,
+ loadingComponent: loadingComponent,
+ errorComponent: errorComponent,
delay = 200,
- timeout // undefined = never times out
+ timeout, // undefined = never times out
+ retryWhen = NO,
+ maxRetries = 3,
+ suspensible = true
} = source
let pendingRequest: Promise<Component> | null = null
let resolvedComp: Component | undefined
+ let retries = 0
+ const retry = (error?: unknown) => {
+ retries++
+ pendingRequest = null
+ return load()
+ }
+
const load = (): Promise<Component> => {
+ let thisRequest: Promise<Component>
return (
pendingRequest ||
- (pendingRequest = loader().then((comp: any) => {
- // interop module default
- if (comp.__esModule || comp[Symbol.toStringTag] === 'Module') {
- comp = comp.default
- }
- if (__DEV__ && !isObject(comp) && !isFunction(comp)) {
- warn(`Invalid async component load result: `, comp)
- }
- resolvedComp = comp
- return comp
- }))
+ (thisRequest = pendingRequest = loader()
+ .catch(err => {
+ err = err instanceof Error ? err : new Error(String(err))
+ if (retryWhen(err) && retries < maxRetries) {
+ return retry(err)
+ } else {
+ throw err
+ }
+ })
+ .then((comp: any) => {
+ if (thisRequest !== pendingRequest && pendingRequest) {
+ return pendingRequest
+ }
+ if (__DEV__ && !comp) {
+ warn(
+ `Async component loader resolved to undefined. ` +
+ `If you are using retry(), make sure to return its return value.`
+ )
+ }
+ // interop module default
+ if (
+ comp &&
+ (comp.__esModule || comp[Symbol.toStringTag] === 'Module')
+ ) {
+ comp = comp.default
+ }
+ if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
+ throw new Error(`Invalid async component load result: ${comp}`)
+ }
+ resolvedComp = comp
+ return comp
+ }))
)
}
})
}
- // TODO hydration
-
const loaded = ref(false)
const error = ref()
const delayed = ref(!!delay)