feat(runtime-core): async component support
This commit is contained in:
155
packages/runtime-core/src/apiAsyncComponent.ts
Normal file
155
packages/runtime-core/src/apiAsyncComponent.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
PublicAPIComponent,
|
||||
Component,
|
||||
currentSuspense,
|
||||
currentInstance,
|
||||
ComponentInternalInstance
|
||||
} from './component'
|
||||
import { isFunction, isObject, EMPTY_OBJ } from '@vue/shared'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { createVNode } from './vnode'
|
||||
import { defineComponent } from './apiDefineComponent'
|
||||
import { warn } from './warning'
|
||||
import { ref } from '@vue/reactivity'
|
||||
import { handleError, ErrorCodes } from './errorHandling'
|
||||
|
||||
export type AsyncComponentResolveResult<T = PublicAPIComponent> =
|
||||
| T
|
||||
| { default: T } // es modules
|
||||
|
||||
export type AsyncComponentLoader<T = any> = () => Promise<
|
||||
AsyncComponentResolveResult<T>
|
||||
>
|
||||
|
||||
export interface AsyncComponentOptions<T = any> {
|
||||
loader: AsyncComponentLoader<T>
|
||||
loading?: PublicAPIComponent
|
||||
error?: PublicAPIComponent
|
||||
delay?: number
|
||||
timeout?: number
|
||||
suspensible?: boolean
|
||||
}
|
||||
|
||||
export function createAsyncComponent<
|
||||
T extends PublicAPIComponent = { new (): ComponentPublicInstance }
|
||||
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
|
||||
if (isFunction(source)) {
|
||||
source = { loader: source }
|
||||
}
|
||||
|
||||
const {
|
||||
suspensible = true,
|
||||
loader,
|
||||
loading: loadingComponent,
|
||||
error: errorComponent,
|
||||
delay = 200,
|
||||
timeout // undefined = never times out
|
||||
} = source
|
||||
|
||||
let pendingRequest: Promise<Component> | null = null
|
||||
let resolvedComp: Component | undefined
|
||||
|
||||
const load = (): 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
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
return defineComponent({
|
||||
name: 'AsyncComponentWrapper',
|
||||
setup() {
|
||||
const instance = currentInstance!
|
||||
|
||||
// already resolved
|
||||
if (resolvedComp) {
|
||||
return () => createInnerComp(resolvedComp!, instance)
|
||||
}
|
||||
|
||||
// suspense-controlled
|
||||
if (__FEATURE_SUSPENSE__ && suspensible && currentSuspense) {
|
||||
return load().then(comp => {
|
||||
return () => createInnerComp(comp, instance)
|
||||
})
|
||||
// TODO suspense error handling
|
||||
}
|
||||
|
||||
// self-controlled
|
||||
if (__NODE_JS__) {
|
||||
// TODO SSR
|
||||
}
|
||||
// TODO hydration
|
||||
|
||||
const loaded = ref(false)
|
||||
const error = ref()
|
||||
const delayed = ref(!!delay)
|
||||
|
||||
if (delay) {
|
||||
setTimeout(() => {
|
||||
delayed.value = false
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (timeout != null) {
|
||||
setTimeout(() => {
|
||||
if (!loaded.value) {
|
||||
const err = new Error(
|
||||
`Async component timed out after ${timeout}ms.`
|
||||
)
|
||||
if (errorComponent) {
|
||||
error.value = err
|
||||
} else {
|
||||
handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
|
||||
}
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
load()
|
||||
.then(() => {
|
||||
loaded.value = true
|
||||
})
|
||||
.catch(err => {
|
||||
pendingRequest = null
|
||||
if (errorComponent) {
|
||||
error.value = err
|
||||
} else {
|
||||
handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (loaded.value && resolvedComp) {
|
||||
return createInnerComp(resolvedComp, instance)
|
||||
} else if (error.value && errorComponent) {
|
||||
return createVNode(errorComponent as Component, {
|
||||
error: error.value
|
||||
})
|
||||
} else if (loadingComponent && !delayed.value) {
|
||||
return createVNode(loadingComponent as Component)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as any
|
||||
}
|
||||
|
||||
function createInnerComp(
|
||||
comp: Component,
|
||||
{ props, slots }: ComponentInternalInstance
|
||||
) {
|
||||
return createVNode(
|
||||
comp,
|
||||
props === EMPTY_OBJ ? null : props,
|
||||
slots === EMPTY_OBJ ? null : slots
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export const enum ErrorCodes {
|
||||
APP_ERROR_HANDLER,
|
||||
APP_WARN_HANDLER,
|
||||
FUNCTION_REF,
|
||||
ASYNC_COMPONENT_LOADER,
|
||||
SCHEDULER
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ export const ErrorTypeStrings: Record<number | string, string> = {
|
||||
[ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
|
||||
[ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
|
||||
[ErrorCodes.FUNCTION_REF]: 'ref function',
|
||||
[ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
|
||||
[ErrorCodes.SCHEDULER]:
|
||||
'scheduler flush. This is likely a Vue internals bug. ' +
|
||||
'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-next'
|
||||
|
||||
@@ -34,6 +34,7 @@ export {
|
||||
export { provide, inject } from './apiInject'
|
||||
export { nextTick } from './scheduler'
|
||||
export { defineComponent } from './apiDefineComponent'
|
||||
export { createAsyncComponent } from './apiAsyncComponent'
|
||||
|
||||
// Advanced API ----------------------------------------------------------------
|
||||
|
||||
@@ -204,4 +205,8 @@ export {
|
||||
} from './directives'
|
||||
export { SuspenseBoundary } from './components/Suspense'
|
||||
export { TransitionState, TransitionHooks } from './components/BaseTransition'
|
||||
export {
|
||||
AsyncComponentOptions,
|
||||
AsyncComponentLoader
|
||||
} from './apiAsyncComponent'
|
||||
export { HMRRuntime } from './hmr'
|
||||
|
||||
Reference in New Issue
Block a user