apiAsyncComponent.ts 6.9 KB

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