Skip to content

Commit 8409f48

Browse files
authored
feat: add defineAsyncComponent API (#644)
1 parent 69ca912 commit 8409f48

File tree

4 files changed

+856
-0
lines changed

4 files changed

+856
-0
lines changed

src/component/defineAsyncComponent.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { isFunction, isObject, warn } from '../utils'
2+
import { VueProxy } from './componentProxy'
3+
import { AsyncComponent } from 'vue'
4+
5+
import {
6+
ComponentOptionsWithoutProps,
7+
ComponentOptionsWithArrayProps,
8+
ComponentOptionsWithProps,
9+
} from './componentOptions'
10+
11+
type ComponentOptions =
12+
| ComponentOptionsWithoutProps
13+
| ComponentOptionsWithArrayProps
14+
| ComponentOptionsWithProps
15+
16+
type Component = VueProxy<any, any, any, any, any>
17+
18+
type ComponentOrComponentOptions = ComponentOptions | Component
19+
20+
export type AsyncComponentResolveResult<T = ComponentOrComponentOptions> =
21+
| T
22+
| { default: T } // es modules
23+
24+
export type AsyncComponentLoader = () => Promise<AsyncComponentResolveResult>
25+
26+
export interface AsyncComponentOptions {
27+
loader: AsyncComponentLoader
28+
loadingComponent?: ComponentOrComponentOptions
29+
errorComponent?: ComponentOrComponentOptions
30+
delay?: number
31+
timeout?: number
32+
suspensible?: boolean
33+
onError?: (
34+
error: Error,
35+
retry: () => void,
36+
fail: () => void,
37+
attempts: number
38+
) => any
39+
}
40+
41+
export function defineAsyncComponent(
42+
source: AsyncComponentLoader | AsyncComponentOptions
43+
): AsyncComponent {
44+
if (isFunction(source)) {
45+
source = { loader: source }
46+
}
47+
48+
const {
49+
loader,
50+
loadingComponent,
51+
errorComponent,
52+
delay = 200,
53+
timeout, // undefined = never times out
54+
suspensible = false, // in Vue 3 default is true
55+
onError: userOnError,
56+
} = source
57+
58+
if (__DEV__ && suspensible) {
59+
warn(
60+
`The suspensiblbe option for async components is not supported in Vue2. It is ignored.`
61+
)
62+
}
63+
64+
let pendingRequest: Promise<Component> | null = null
65+
66+
let retries = 0
67+
const retry = () => {
68+
retries++
69+
pendingRequest = null
70+
return load()
71+
}
72+
73+
const load = (): Promise<ComponentOrComponentOptions> => {
74+
let thisRequest: Promise<ComponentOrComponentOptions>
75+
return (
76+
pendingRequest ||
77+
(thisRequest = pendingRequest = loader()
78+
.catch((err) => {
79+
err = err instanceof Error ? err : new Error(String(err))
80+
if (userOnError) {
81+
return new Promise((resolve, reject) => {
82+
const userRetry = () => resolve(retry())
83+
const userFail = () => reject(err)
84+
userOnError(err, userRetry, userFail, retries + 1)
85+
})
86+
} else {
87+
throw err
88+
}
89+
})
90+
.then((comp: any) => {
91+
if (thisRequest !== pendingRequest && pendingRequest) {
92+
return pendingRequest
93+
}
94+
if (__DEV__ && !comp) {
95+
warn(
96+
`Async component loader resolved to undefined. ` +
97+
`If you are using retry(), make sure to return its return value.`
98+
)
99+
}
100+
// interop module default
101+
if (
102+
comp &&
103+
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
104+
) {
105+
comp = comp.default
106+
}
107+
if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
108+
throw new Error(`Invalid async component load result: ${comp}`)
109+
}
110+
return comp
111+
}))
112+
)
113+
}
114+
115+
return () => {
116+
const component = load()
117+
118+
return {
119+
component: component as any, // there is a type missmatch between vue2 type and the docs
120+
delay,
121+
timeout,
122+
error: errorComponent,
123+
loading: loadingComponent,
124+
}
125+
}
126+
}

src/component/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { defineComponent } from './defineComponent'
2+
export { defineAsyncComponent } from './defineAsyncComponent'
23
export { SetupFunction, SetupContext } from './componentOptions'
34
export { ComponentInstance, ComponentRenderProxy } from './componentProxy'
45
export { Data } from './common'
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { AsyncComponent } from 'vue'
2+
import { defineAsyncComponent, defineComponent, expectType } from './index'
3+
4+
function asyncComponent1() {
5+
return Promise.resolve().then(() => {
6+
return defineComponent({})
7+
})
8+
}
9+
10+
function asyncComponent2() {
11+
return Promise.resolve().then(() => {
12+
return {
13+
template: 'ASYNC',
14+
}
15+
})
16+
}
17+
18+
const syncComponent1 = defineComponent({
19+
template: '',
20+
})
21+
22+
const syncComponent2 = {
23+
template: '',
24+
}
25+
26+
defineAsyncComponent(asyncComponent1)
27+
defineAsyncComponent(asyncComponent2)
28+
29+
defineAsyncComponent({
30+
loader: asyncComponent1,
31+
delay: 200,
32+
timeout: 3000,
33+
errorComponent: syncComponent1,
34+
loadingComponent: syncComponent1,
35+
})
36+
37+
defineAsyncComponent({
38+
loader: asyncComponent2,
39+
delay: 200,
40+
timeout: 3000,
41+
errorComponent: syncComponent2,
42+
loadingComponent: syncComponent2,
43+
})
44+
45+
defineAsyncComponent(
46+
() =>
47+
new Promise((resolve, reject) => {
48+
resolve(syncComponent1)
49+
})
50+
)
51+
52+
defineAsyncComponent(
53+
() =>
54+
new Promise((resolve, reject) => {
55+
resolve(syncComponent2)
56+
})
57+
)
58+
59+
const component = defineAsyncComponent({
60+
loader: asyncComponent1,
61+
loadingComponent: defineComponent({}),
62+
errorComponent: defineComponent({}),
63+
delay: 200,
64+
timeout: 3000,
65+
suspensible: false,
66+
onError(error, retry, fail, attempts) {
67+
expectType<() => void>(retry)
68+
expectType<() => void>(fail)
69+
expectType<number>(attempts)
70+
expectType<Error>(error)
71+
},
72+
})
73+
74+
expectType<AsyncComponent>(component)

0 commit comments

Comments
 (0)