apiAsyncComponent.ts 6.2 KB

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