apiAsyncComponent.ts 8.0 KB

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