apiAsyncComponent.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import {
  2. type Component,
  3. type ComponentInternalInstance,
  4. type ComponentOptions,
  5. type ConcreteComponent,
  6. currentInstance,
  7. isInSSRComponentSetup,
  8. } from './component'
  9. import { isFunction, isObject } from '@vue/shared'
  10. import type { ComponentPublicInstance } from './componentPublicInstance'
  11. import { type VNode, createVNode } from './vnode'
  12. import { defineComponent } from './apiDefineComponent'
  13. import { warn } from './warning'
  14. import { ref } from '@vue/reactivity'
  15. import { ErrorCodes, handleError } from './errorHandling'
  16. import { isKeepAlive } from './components/KeepAlive'
  17. import { queueJob } from './scheduler'
  18. export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
  19. export type AsyncComponentLoader<T = any> = () => Promise<
  20. AsyncComponentResolveResult<T>
  21. >
  22. export interface AsyncComponentOptions<T = any> {
  23. loader: AsyncComponentLoader<T>
  24. loadingComponent?: Component
  25. errorComponent?: Component
  26. delay?: number
  27. timeout?: number
  28. suspensible?: boolean
  29. onError?: (
  30. error: Error,
  31. retry: () => void,
  32. fail: () => void,
  33. attempts: number,
  34. ) => any
  35. }
  36. export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean =>
  37. !!(i.type as ComponentOptions).__asyncLoader
  38. /*! #__NO_SIDE_EFFECTS__ */
  39. export function defineAsyncComponent<
  40. T extends Component = { new (): ComponentPublicInstance },
  41. >(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
  42. if (isFunction(source)) {
  43. source = { loader: source }
  44. }
  45. const {
  46. loader,
  47. loadingComponent,
  48. errorComponent,
  49. delay = 200,
  50. timeout, // undefined = never times out
  51. suspensible = true,
  52. onError: userOnError,
  53. } = source
  54. let pendingRequest: Promise<ConcreteComponent> | null = null
  55. let resolvedComp: ConcreteComponent | undefined
  56. let retries = 0
  57. const retry = () => {
  58. retries++
  59. pendingRequest = null
  60. return load()
  61. }
  62. const load = (): Promise<ConcreteComponent> => {
  63. let thisRequest: Promise<ConcreteComponent>
  64. return (
  65. pendingRequest ||
  66. (thisRequest = pendingRequest =
  67. loader()
  68. .catch(err => {
  69. err = err instanceof Error ? err : new Error(String(err))
  70. if (userOnError) {
  71. return new Promise((resolve, reject) => {
  72. const userRetry = () => resolve(retry())
  73. const userFail = () => reject(err)
  74. userOnError(err, userRetry, userFail, retries + 1)
  75. })
  76. } else {
  77. throw err
  78. }
  79. })
  80. .then((comp: any) => {
  81. if (thisRequest !== pendingRequest && pendingRequest) {
  82. return pendingRequest
  83. }
  84. if (__DEV__ && !comp) {
  85. warn(
  86. `Async component loader resolved to undefined. ` +
  87. `If you are using retry(), make sure to return its return value.`,
  88. )
  89. }
  90. // interop module default
  91. if (
  92. comp &&
  93. (comp.__esModule || comp[Symbol.toStringTag] === 'Module')
  94. ) {
  95. comp = comp.default
  96. }
  97. if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
  98. throw new Error(`Invalid async component load result: ${comp}`)
  99. }
  100. resolvedComp = comp
  101. return comp
  102. }))
  103. )
  104. }
  105. return defineComponent({
  106. name: 'AsyncComponentWrapper',
  107. __asyncLoader: load,
  108. get __asyncResolved() {
  109. return resolvedComp
  110. },
  111. setup() {
  112. const instance = currentInstance!
  113. // already resolved
  114. if (resolvedComp) {
  115. return () => createInnerComp(resolvedComp!, instance)
  116. }
  117. const onError = (err: Error) => {
  118. pendingRequest = null
  119. handleError(
  120. err,
  121. instance,
  122. ErrorCodes.ASYNC_COMPONENT_LOADER,
  123. !errorComponent /* do not throw in dev if user provided error component */,
  124. )
  125. }
  126. // suspense-controlled or SSR.
  127. if (
  128. (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) ||
  129. (__SSR__ && isInSSRComponentSetup)
  130. ) {
  131. return load()
  132. .then(comp => {
  133. return () => createInnerComp(comp, instance)
  134. })
  135. .catch(err => {
  136. onError(err)
  137. return () =>
  138. errorComponent
  139. ? createVNode(errorComponent as ConcreteComponent, {
  140. error: err,
  141. })
  142. : null
  143. })
  144. }
  145. const loaded = ref(false)
  146. const error = ref()
  147. const delayed = ref(!!delay)
  148. if (delay) {
  149. setTimeout(() => {
  150. delayed.value = false
  151. }, delay)
  152. }
  153. if (timeout != null) {
  154. setTimeout(() => {
  155. if (!loaded.value && !error.value) {
  156. const err = new Error(
  157. `Async component timed out after ${timeout}ms.`,
  158. )
  159. onError(err)
  160. error.value = err
  161. }
  162. }, timeout)
  163. }
  164. load()
  165. .then(() => {
  166. loaded.value = true
  167. if (instance.parent && isKeepAlive(instance.parent.vnode)) {
  168. // parent is keep-alive, force update so the loaded component's
  169. // name is taken into account
  170. instance.parent.effect.dirty = true
  171. queueJob(instance.parent.update)
  172. }
  173. })
  174. .catch(err => {
  175. onError(err)
  176. error.value = err
  177. })
  178. return () => {
  179. if (loaded.value && resolvedComp) {
  180. return createInnerComp(resolvedComp, instance)
  181. } else if (error.value && errorComponent) {
  182. return createVNode(errorComponent, {
  183. error: error.value,
  184. })
  185. } else if (loadingComponent && !delayed.value) {
  186. return createVNode(loadingComponent)
  187. }
  188. }
  189. },
  190. }) as T
  191. }
  192. function createInnerComp(
  193. comp: ConcreteComponent,
  194. parent: ComponentInternalInstance,
  195. ) {
  196. const { ref, props, children, ce } = parent.vnode
  197. const vnode = createVNode(comp, props, children)
  198. // ensure inner component inherits the async wrapper's ref owner
  199. vnode.ref = ref
  200. // pass the custom element callback on to the inner comp
  201. // and remove it from the async wrapper
  202. vnode.ce = ce
  203. delete parent.vnode.ce
  204. return vnode
  205. }