Explorar o código

fix(runtime-vapor): support render vapor async wrapper in vdom suspense

daiwei hai 1 mes
pai
achega
3b2167d1c2

+ 150 - 1
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -2235,7 +2235,156 @@ describe('vdomInterop', () => {
   })
 
   describe('Suspense', () => {
-    test('renders async vapor child inside VDOM Suspense', async () => {
+    test('renders vapor async wrapper inside VDOM Suspense', async () => {
+      const duration = 5
+
+      const VaporAsyncChild = defineVaporAsyncComponent({
+        loader: () =>
+          new Promise(resolve => {
+            setTimeout(() => {
+              resolve(
+                defineVaporComponent({
+                  setup() {
+                    return template('<div><button>click</button></div>')()
+                  },
+                }) as any,
+              )
+            }, duration)
+          }),
+      })
+
+      const VaporParent = defineVaporComponent({
+        setup() {
+          return createComponent(
+            Suspense as any,
+            null,
+            {
+              default: () => createComponent(VaporAsyncChild, null, null, true),
+              fallback: () => template('loading')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporParent as any)
+        },
+      }).render()
+
+      expect(html()).toContain('loading')
+
+      await new Promise(resolve => setTimeout(resolve, duration + 1))
+      await nextTick()
+
+      expect(html()).toContain('<div><button>click</button></div>')
+    })
+
+    test('does not suspend vapor async wrapper with suspensible false inside VDOM Suspense', async () => {
+      const duration = 5
+
+      const VaporAsyncChild = defineVaporAsyncComponent({
+        loader: () =>
+          new Promise(resolve => {
+            setTimeout(() => {
+              resolve(
+                defineVaporComponent({
+                  setup() {
+                    return template('<div><button>click</button></div>')()
+                  },
+                }) as any,
+              )
+            }, duration)
+          }),
+        suspensible: false,
+      })
+
+      const VaporParent = defineVaporComponent({
+        setup() {
+          return createComponent(
+            Suspense as any,
+            null,
+            {
+              default: () => createComponent(VaporAsyncChild, null, null, true),
+              fallback: () => template('loading')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporParent as any)
+        },
+      }).render()
+
+      expect(html()).not.toContain('loading')
+      expect(html()).toContain('<!--async component-->')
+
+      await new Promise(resolve => setTimeout(resolve, duration + 1))
+      await nextTick()
+
+      expect(html()).toContain('<div><button>click</button></div>')
+    })
+
+    test('renders error component for vapor async wrapper inside VDOM Suspense', async () => {
+      const tick = () => new Promise(resolve => setTimeout(resolve))
+
+      let reject!: (error: Error) => void
+      const VaporAsyncChild = defineVaporAsyncComponent({
+        loader: () =>
+          new Promise((_resolve, _reject) => {
+            reject = _reject as (error: Error) => void
+          }),
+        errorComponent: defineVaporComponent({
+          props: ['error'],
+          setup(props: { error: Error }) {
+            return template(props.error.message)()
+          },
+        }),
+      })
+
+      const VaporParent = defineVaporComponent({
+        setup() {
+          return createComponent(
+            Suspense as any,
+            null,
+            {
+              default: () => createComponent(VaporAsyncChild, null, null, true),
+              fallback: () => template('loading')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const host = document.createElement('div')
+      const app = createApp({
+        render: () => h(VaporParent as any),
+      })
+      const errorHandler = vi.fn()
+      app.use(vaporInteropPlugin)
+      app.config.errorHandler = errorHandler
+      try {
+        app.mount(host)
+
+        expect(host.innerHTML).toContain('loading')
+
+        reject(new Error('errored out'))
+        await tick()
+        await nextTick()
+
+        expect(errorHandler).toHaveBeenCalled()
+        expect(host.innerHTML).toContain('errored out')
+      } finally {
+        app.unmount()
+        host.remove()
+      }
+    })
+
+    test('renders async setup vapor component inside VDOM Suspense', async () => {
       const duration = 5
 
       const VaporAsyncChild = defineVaporComponent({

+ 39 - 17
packages/runtime-vapor/src/apiDefineAsyncComponent.ts

@@ -7,6 +7,7 @@ import {
   handleError,
   markAsyncBoundary,
   performAsyncHydrate,
+  setCurrentInstance,
   useAsyncComponentState,
   watch,
 } from '@vue/runtime-dom'
@@ -153,7 +154,7 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
       // already resolved
       let resolvedComp = getResolvedComp()
       if (resolvedComp) {
-        frag!.update(() => createInnerComp(resolvedComp!, instance, frag))
+        frag!.update(() => createInnerComp(resolvedComp!, instance))
         return frag
       }
 
@@ -167,8 +168,26 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
         )
       }
 
-      // TODO suspense-controlled
       if (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) {
+        return load()
+          .then(() => {
+            resolvedComp = getResolvedComp()
+            if (resolvedComp) {
+              frag.update(() => createInnerComp(resolvedComp!, instance))
+            }
+            return frag
+          })
+          .catch(err => {
+            onError(err)
+            if (errorComponent) {
+              frag.update(() =>
+                createInnerComp(errorComponent, instance, {
+                  error: () => err,
+                }),
+              )
+            }
+            return frag
+          })
       }
 
       const { loaded, error, delayed } = useAsyncComponentState(
@@ -190,7 +209,7 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
         resolvedComp = getResolvedComp()
         let render
         if (loaded.value && resolvedComp) {
-          render = () => createInnerComp(resolvedComp!, instance, frag)
+          render = () => createInnerComp(resolvedComp!, instance)
         } else if (error.value && errorComponent) {
           render = () =>
             createComponent(errorComponent, { error: () => error.value })
@@ -211,19 +230,22 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
 function createInnerComp(
   comp: VaporComponent,
   parent: VaporComponentInstance & TransitionOptions,
-  frag?: DynamicFragment,
+  rawProps = parent.rawProps,
+  rawSlots = parent.rawSlots,
 ): VaporComponentInstance {
-  const { rawProps, rawSlots, appContext } = parent
-  const instance = createComponent(
-    comp,
-    rawProps,
-    rawSlots,
-    // rawProps is shared and already contains fallthrough attrs.
-    // so isSingleRoot should be undefined
-    undefined,
-    undefined,
-    appContext,
-  )
-
-  return instance
+  const prevInstance = setCurrentInstance(parent)
+  try {
+    return createComponent(
+      comp,
+      rawProps,
+      rawSlots,
+      // rawProps is shared and already contains fallthrough attrs.
+      // so isSingleRoot should be undefined
+      undefined,
+      undefined,
+      parent.appContext,
+    )
+  } finally {
+    setCurrentInstance(...prevInstance)
+  }
 }