Skip to content

Commit

Permalink
feat(runtime-vapor): init async component
Browse files Browse the repository at this point in the history
  • Loading branch information
Doctor-wu committed Jun 18, 2024
1 parent bc04592 commit 83e7116
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 1 deletion.
251 changes: 251 additions & 0 deletions packages/runtime-vapor/src/apiAsyncComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import {
type Component,
type ComponentInternalInstance,
currentInstance,
getCurrentInstance,
} from './component'
import { isFunction, isObject } from '@vue/shared'
import { defineComponent } from './apiDefineComponent'
import { warn } from './warning'
import { ref } from '@vue/reactivity'
import { VaporErrorCodes, handleError } from './errorHandling'
// import { isKeepAlive } from './components/KeepAlive'
import { queueJob } from './scheduler'

Check failure on line 13 in packages/runtime-vapor/src/apiAsyncComponent.ts

View workflow job for this annotation

GitHub Actions / lint-and-test-dts

'queueJob' is declared but its value is never read.
import { createComponent } from './apiCreateComponent'
import { renderEffect } from './renderEffect'

Check failure on line 15 in packages/runtime-vapor/src/apiAsyncComponent.ts

View workflow job for this annotation

GitHub Actions / lint-and-test-dts

'renderEffect' is declared but its value is never read.
import { createIf } from './apiCreateIf'
import { template } from '@vue/vapor'

Check failure on line 17 in packages/runtime-vapor/src/apiAsyncComponent.ts

View workflow job for this annotation

GitHub Actions / lint-and-test-dts

'template' is declared but its value is never read.

export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules

export type AsyncComponentLoader<T = any> = () => Promise<
AsyncComponentResolveResult<T>
>

export interface AsyncComponentOptions<T = any> {
loader: AsyncComponentLoader<T>
loadingComponent?: Component
errorComponent?: Component
delay?: number
timeout?: number
suspensible?: boolean
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number,
) => any
}

export const isAsyncWrapper = (i: ComponentInternalInstance): boolean =>
!!i.component.__asyncLoader

/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<T extends Component = Component>(
source: AsyncComponentLoader<T> | AsyncComponentOptions<T>,
): T {
if (isFunction(source)) {
source = { loader: source }
}

const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout, // undefined = never times out
suspensible = true,

Check failure on line 57 in packages/runtime-vapor/src/apiAsyncComponent.ts

View workflow job for this annotation

GitHub Actions / lint-and-test-dts

'suspensible' is declared but its value is never read.
onError: userOnError,
} = source

let pendingRequest: Promise<Component> | null = null
let resolvedComp: Component | undefined

let retries = 0
const retry = () => {
retries++
pendingRequest = null
return load()
}

const load = (): Promise<Component> => {
let thisRequest: Promise<Component>
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}`)
}
resolvedComp = comp
return comp
}))
)
}

return defineComponent({
name: 'AsyncComponentWrapper',

__asyncLoader: load,

get __asyncResolved() {
return resolvedComp
},

setup() {
const instance = currentInstance!

// already resolved
if (resolvedComp) {
return createInnerComp(resolvedComp!, instance)
}

const onError = (err: Error) => {
pendingRequest = null
handleError(
err,
instance,
VaporErrorCodes.ASYNC_COMPONENT_LOADER,
!errorComponent /* do not throw in dev if user provided error component */,
)
}

// TODO: handle suspense and SSR.
// suspense-controlled or SSR.
// if (
// (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) ||
// (__SSR__ && isInSSRComponentSetup)
// ) {
// return load()
// .then(comp => {
// return () => createInnerComp(comp, instance)
// })
// .catch(err => {
// onError(err)
// return () =>
// errorComponent
// ? createVNode(errorComponent as ConcreteComponent, {
// error: err,
// })
// : null
// })
// }

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 && !error.value) {
const err = new Error(
`Async component timed out after ${timeout}ms.`,
)
onError(err)
error.value = err
}
}, timeout)
}

load()
.then(() => {
loaded.value = true
// TODO: handle keep-alive.
// if (instance.parent && isKeepAlive(instance.parent.vnode)) {
// // parent is keep-alive, force update so the loaded component's
// // name is taken into account
// queueJob(instance.parent.update)
// }
})
.catch(err => {
onError(err)
error.value = err
})

// if (loaded.value && resolvedComp) {
// return createInnerComp(resolvedComp, instance)
// } else if (error.value && errorComponent) {
// return createComponent(errorComponent, [{ error: () => error.value }])
// } else if (loadingComponent && !delayed.value) {
// return createComponent(loadingComponent)
// }
return {
loaded,
error,
delayed,
}
},
render(ctx) {
const instance = getCurrentInstance()!
return [
createIf(
() => ctx.loaded && resolvedComp,
() => {
return createInnerComp(resolvedComp!, instance)
},
() =>
createIf(
() => ctx.error && errorComponent,
() =>
createComponent(errorComponent!, [{ error: () => ctx.error }]),
() =>
createIf(
() => loadingComponent && !ctx.delayed,
() => createComponent(loadingComponent!),
),
),
),
]
},
}) as T
}

function createInnerComp(comp: Component, parent: ComponentInternalInstance) {
const { rawProps: props, rawSlots, rawDynamicSlots } = parent
const innerComp = createComponent(comp, props, rawSlots, rawDynamicSlots)
// const vnode = createVNode(comp, props, children)
// // ensure inner component inherits the async wrapper's ref owner
innerComp.refs = parent.refs
// vnode.ref = ref
// // pass the custom element callback on to the inner comp
// // and remove it from the async wrapper
// vnode.ce = ce
// delete parent.vnode.ce

return innerComp
}
15 changes: 15 additions & 0 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ export interface ObjectComponent extends ComponentInternalOptions {
emits?: EmitsOptions
render?(ctx: any): Block

/**
* marker for AsyncComponentWrapper
* @internal
*/
__asyncLoader?: () => Promise<Component>
/**
* the inner component resolved by the AsyncComponentWrapper
* @internal
*/
__asyncResolved?: Component

name?: string
vapor?: boolean
}
Expand Down Expand Up @@ -179,6 +190,8 @@ export interface ComponentInternalInstance {
emit: EmitFn
emitted: Record<string, boolean> | null
attrs: Data
rawSlots: InternalSlots
rawDynamicSlots: DynamicSlots | null
slots: InternalSlots
refs: Data
// exposed properties via expose()
Expand Down Expand Up @@ -304,6 +317,8 @@ export function createComponentInstance(
emit: null!,
emitted: null,
attrs: EMPTY_OBJ,
rawSlots: slots || EMPTY_OBJ,
rawDynamicSlots: dynamicSlots || null,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,

Expand Down
6 changes: 5 additions & 1 deletion packages/runtime-vapor/src/dom/templateRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@vue/shared'
import { warn } from '../warning'
import { queuePostFlushCb } from '../scheduler'
import { isAsyncWrapper } from '../apiAsyncComponent'

export type NodeRef = string | Ref | ((ref: Element) => void)
export type RefEl = Element | ComponentInternalInstance
Expand All @@ -36,11 +37,14 @@ export function setRef(
if (!currentInstance) return
const { setupState, isUnmounted } = currentInstance

const isComponent = isVaporComponent(el)
const isAsync = isComponent && isAsyncWrapper(currentInstance)

Check failure on line 41 in packages/runtime-vapor/src/dom/templateRef.ts

View workflow job for this annotation

GitHub Actions / lint-and-test-dts

'isAsync' is declared but its value is never read.

if (isUnmounted) {
return
}

const refValue = isVaporComponent(el) ? el.exposed || el : el
const refValue = isComponent ? el.exposed || el : el

const refs =
currentInstance.refs === EMPTY_OBJ
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-vapor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
export { setRef } from './dom/templateRef'

export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent'
export {
type InjectionKey,
inject,
Expand Down

0 comments on commit 83e7116

Please sign in to comment.