apiAsyncComponent.ts 5.6 KB

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