apiAsyncComponent.ts 5.3 KB

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