-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
close #12608
- Loading branch information
Showing
6 changed files
with
405 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { warn, isFunction, isObject } from 'core/util' | ||
|
||
interface AsyncComponentOptions { | ||
loader: Function | ||
loadingComponent?: any | ||
errorComponent?: any | ||
delay?: number | ||
timeout?: number | ||
suspensible?: boolean | ||
onError?: ( | ||
error: Error, | ||
retry: () => void, | ||
fail: () => void, | ||
attempts: number | ||
) => any | ||
} | ||
|
||
type AsyncComponentFactory = () => { | ||
component: Promise<any> | ||
loading?: any | ||
error?: any | ||
delay?: number | ||
timeout?: number | ||
} | ||
|
||
/** | ||
* v3-compatible async component API. | ||
* @internal the type is manually declared in <root>/types/v3-define-async-component.d.ts | ||
* because it relies on existing manual types | ||
*/ | ||
export function defineAsyncComponent( | ||
source: (() => any) | AsyncComponentOptions | ||
): AsyncComponentFactory { | ||
if (isFunction(source)) { | ||
source = { loader: source } as AsyncComponentOptions | ||
} | ||
|
||
const { | ||
loader, | ||
loadingComponent, | ||
errorComponent, | ||
delay = 200, | ||
timeout, // undefined = never times out | ||
suspensible = false, // in Vue 3 default is true | ||
onError: userOnError | ||
} = source | ||
|
||
if (__DEV__ && suspensible) { | ||
warn( | ||
`The suspensiblbe option for async components is not supported in Vue2. It is ignored.` | ||
) | ||
} | ||
|
||
let pendingRequest: Promise<any> | null = null | ||
|
||
let retries = 0 | ||
const retry = () => { | ||
retries++ | ||
pendingRequest = null | ||
return load() | ||
} | ||
|
||
const load = (): Promise<any> => { | ||
let thisRequest: Promise<any> | ||
return ( | ||
pendingRequest || | ||
(thisRequest = pendingRequest = | ||
loader() | ||
.catch(err => { | ||
err = err instanceof Error ? err : new Error(String(err)) | ||
if (userOnError) { | ||
return new Promise((resolve, reject) => { | ||
const userRetry = () => resolve(retry()) | ||
const userFail = () => reject(err) | ||
userOnError(err, userRetry, userFail, retries + 1) | ||
}) | ||
} 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}`) | ||
} | ||
return comp | ||
})) | ||
) | ||
} | ||
|
||
return () => { | ||
const component = load() | ||
|
||
return { | ||
component, | ||
delay, | ||
timeout, | ||
error: errorComponent, | ||
loading: loadingComponent | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
import Vue from 'vue' | ||
import { defineAsyncComponent, h, ref, nextTick, defineComponent } from 'v3' | ||
import { Component } from 'types/component' | ||
|
||
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) | ||
|
||
const loadingComponent = defineComponent({ | ||
template: `<div>loading</div>` | ||
}) | ||
|
||
const resolvedComponent = defineComponent({ | ||
template: `<div>resolved</div>` | ||
}) | ||
|
||
describe('api: defineAsyncComponent', () => { | ||
afterEach(() => { | ||
Vue.config.errorHandler = undefined | ||
}) | ||
|
||
test('simple usage', async () => { | ||
let resolve: (comp: Component) => void | ||
const Foo = defineAsyncComponent( | ||
() => | ||
new Promise(r => { | ||
resolve = r as any | ||
}) | ||
) | ||
|
||
const toggle = ref(true) | ||
|
||
const vm = new Vue({ | ||
render: () => (toggle.value ? h(Foo) : null) | ||
}).$mount() | ||
|
||
expect(vm.$el.nodeType).toBe(8) | ||
|
||
resolve!(resolvedComponent) | ||
// first time resolve, wait for macro task since there are multiple | ||
// microtasks / .then() calls | ||
await timeout() | ||
expect(vm.$el.innerHTML).toBe('resolved') | ||
|
||
toggle.value = false | ||
await nextTick() | ||
expect(vm.$el.nodeType).toBe(8) | ||
|
||
// already resolved component should update on nextTick | ||
toggle.value = true | ||
await nextTick() | ||
expect(vm.$el.innerHTML).toBe('resolved') | ||
}) | ||
|
||
test('with loading component', async () => { | ||
let resolve: (comp: Component) => void | ||
const Foo = defineAsyncComponent({ | ||
loader: () => | ||
new Promise(r => { | ||
resolve = r as any | ||
}), | ||
loadingComponent, | ||
delay: 1 // defaults to 200 | ||
}) | ||
|
||
const toggle = ref(true) | ||
|
||
const vm = new Vue({ | ||
render: () => (toggle.value ? h(Foo) : null) | ||
}).$mount() | ||
|
||
// due to the delay, initial mount should be empty | ||
expect(vm.$el.nodeType).toBe(8) | ||
|
||
// loading show up after delay | ||
await timeout(1) | ||
expect(vm.$el.innerHTML).toBe('loading') | ||
|
||
resolve!(resolvedComponent) | ||
await timeout() | ||
expect(vm.$el.innerHTML).toBe('resolved') | ||
|
||
toggle.value = false | ||
await nextTick() | ||
expect(vm.$el.nodeType).toBe(8) | ||
|
||
// already resolved component should update on nextTick without loading | ||
// state | ||
toggle.value = true | ||
await nextTick() | ||
expect(vm.$el.innerHTML).toBe('resolved') | ||
}) | ||
|
||
test('error with error component', async () => { | ||
let reject: (e: Error) => void | ||
const Foo = defineAsyncComponent({ | ||
loader: () => | ||
new Promise((_resolve, _reject) => { | ||
reject = _reject | ||
}), | ||
errorComponent: { | ||
template: `<div>errored</div>` | ||
} | ||
}) | ||
|
||
const toggle = ref(true) | ||
|
||
const vm = new Vue({ | ||
render: () => (toggle.value ? h(Foo) : null) | ||
}).$mount() | ||
|
||
expect(vm.$el.nodeType).toBe(8) | ||
|
||
const err = new Error('errored') | ||
reject!(err) | ||
await timeout() | ||
expect('Failed to resolve async').toHaveBeenWarned() | ||
expect(vm.$el.innerHTML).toBe('errored') | ||
|
||
toggle.value = false | ||
await nextTick() | ||
expect(vm.$el.nodeType).toBe(8) | ||
}) | ||
|
||
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 | ||
}) | ||
}, | ||
onError(error, retry, fail) { | ||
if (error.message.match(/foo/)) { | ||
retry() | ||
} else { | ||
fail() | ||
} | ||
} | ||
}) | ||
|
||
const vm = new Vue({ | ||
render: () => h(Foo) | ||
}).$mount() | ||
|
||
expect(vm.$el.nodeType).toBe(8) | ||
expect(loaderCallCount).toBe(1) | ||
|
||
const err = new Error('foo') | ||
reject!(err) | ||
await timeout() | ||
expect(loaderCallCount).toBe(2) | ||
expect(vm.$el.nodeType).toBe(8) | ||
|
||
// should render this time | ||
resolve!(resolvedComponent) | ||
await timeout() | ||
expect(vm.$el.innerHTML).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 | ||
}) | ||
}, | ||
onError(error, retry, fail) { | ||
if (error.message.match(/bar/)) { | ||
retry() | ||
} else { | ||
fail() | ||
} | ||
} | ||
}) | ||
|
||
const vm = new Vue({ | ||
render: () => h(Foo) | ||
}).$mount() | ||
|
||
expect(vm.$el.nodeType).toBe(8) | ||
expect(loaderCallCount).toBe(1) | ||
|
||
const err = new Error('foo') | ||
reject!(err) | ||
await timeout() | ||
// should fail because retryWhen returns false | ||
expect(loaderCallCount).toBe(1) | ||
expect(vm.$el.nodeType).toBe(8) | ||
expect('Failed to resolve async').toHaveBeenWarned() | ||
}) | ||
|
||
test('retry (fail w/ max retry attempts)', async () => { | ||
let loaderCallCount = 0 | ||
let reject: (e: Error) => void | ||
|
||
const Foo = defineAsyncComponent({ | ||
loader: () => { | ||
loaderCallCount++ | ||
return new Promise((_resolve, _reject) => { | ||
reject = _reject | ||
}) | ||
}, | ||
onError(error, retry, fail, attempts) { | ||
if (error.message.match(/foo/) && attempts <= 1) { | ||
retry() | ||
} else { | ||
fail() | ||
} | ||
} | ||
}) | ||
|
||
const vm = new Vue({ | ||
render: () => h(Foo) | ||
}).$mount() | ||
|
||
expect(vm.$el.nodeType).toBe(8) | ||
expect(loaderCallCount).toBe(1) | ||
|
||
// first retry | ||
const err = new Error('foo') | ||
reject!(err) | ||
await timeout() | ||
expect(loaderCallCount).toBe(2) | ||
expect(vm.$el.nodeType).toBe(8) | ||
|
||
// 2nd retry, should fail due to reaching maxRetries | ||
reject!(err) | ||
await timeout() | ||
expect(loaderCallCount).toBe(2) | ||
expect(vm.$el.nodeType).toBe(8) | ||
expect('Failed to resolve async').toHaveBeenWarned() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { defineAsyncComponent } from '../../v3-define-async-component' | ||
import { defineComponent } from '../../v3-define-component' | ||
|
||
defineAsyncComponent(() => Promise.resolve({})) | ||
|
||
// @ts-expect-error | ||
defineAsyncComponent({}) | ||
|
||
defineAsyncComponent({ | ||
loader: () => Promise.resolve({}), | ||
loadingComponent: defineComponent({}), | ||
errorComponent: defineComponent({}), | ||
delay: 123, | ||
timeout: 3000, | ||
onError(err, retry, fail, attempts) { | ||
retry() | ||
fail() | ||
} | ||
}) |
Oops, something went wrong.