Sfoglia il codice sorgente

feat(server-renderer): render suspense in vnode mode (#727)

Dmitry Sharshakov 6 anni fa
parent
commit
589aeb402c

+ 1 - 1
packages/runtime-core/src/components/Suspense.ts

@@ -449,7 +449,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
   return suspense
 }
 
-function normalizeSuspenseChildren(
+export function normalizeSuspenseChildren(
   vnode: VNode
 ): {
   content: VNode

+ 3 - 1
packages/runtime-core/src/index.ts

@@ -114,6 +114,7 @@ import {
   setCurrentRenderingInstance
 } from './componentRenderUtils'
 import { isVNode, normalizeVNode } from './vnode'
+import { normalizeSuspenseChildren } from './components/Suspense'
 
 // SSR utils are only exposed in cjs builds.
 const _ssrUtils = {
@@ -122,7 +123,8 @@ const _ssrUtils = {
   renderComponentRoot,
   setCurrentRenderingInstance,
   isVNode,
-  normalizeVNode
+  normalizeVNode,
+  normalizeSuspenseChildren
 }
 
 export const ssrUtils = (__NODE_JS__ ? _ssrUtils : null) as typeof _ssrUtils

+ 110 - 0
packages/server-renderer/__tests__/ssrSuspense.spec.ts

@@ -0,0 +1,110 @@
+import { createApp, h, Suspense } from 'vue'
+import { renderToString } from '../src/renderToString'
+
+describe('SSR Suspense', () => {
+  const ResolvingAsync = {
+    async setup() {
+      return () => h('div', 'async')
+    }
+  }
+
+  const RejectingAsync = {
+    setup() {
+      return new Promise((_, reject) => reject())
+    }
+  }
+
+  test('render', async () => {
+    const Comp = {
+      render() {
+        return h(Suspense, null, {
+          default: h(ResolvingAsync),
+          fallback: h('div', 'fallback')
+        })
+      }
+    }
+
+    expect(await renderToString(createApp(Comp))).toBe(`<div>async</div>`)
+  })
+
+  test('fallback', async () => {
+    const Comp = {
+      render() {
+        return h(Suspense, null, {
+          default: h(RejectingAsync),
+          fallback: h('div', 'fallback')
+        })
+      }
+    }
+
+    expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
+  })
+
+  test('2 components', async () => {
+    const Comp = {
+      render() {
+        return h(Suspense, null, {
+          default: h('div', [h(ResolvingAsync), h(ResolvingAsync)]),
+          fallback: h('div', 'fallback')
+        })
+      }
+    }
+
+    expect(await renderToString(createApp(Comp))).toBe(
+      `<div><div>async</div><div>async</div></div>`
+    )
+  })
+
+  test('resolving component + rejecting component', async () => {
+    const Comp = {
+      render() {
+        return h(Suspense, null, {
+          default: h('div', [h(ResolvingAsync), h(RejectingAsync)]),
+          fallback: h('div', 'fallback')
+        })
+      }
+    }
+
+    expect(await renderToString(createApp(Comp))).toBe(`<div>fallback</div>`)
+  })
+
+  test('failing suspense in passing suspense', async () => {
+    const Comp = {
+      render() {
+        return h(Suspense, null, {
+          default: h('div', [
+            h(ResolvingAsync),
+            h(Suspense, null, {
+              default: h('div', [h(RejectingAsync)]),
+              fallback: h('div', 'fallback 2')
+            })
+          ]),
+          fallback: h('div', 'fallback 1')
+        })
+      }
+    }
+
+    expect(await renderToString(createApp(Comp))).toBe(
+      `<div><div>async</div><div>fallback 2</div></div>`
+    )
+  })
+
+  test('passing suspense in failing suspense', async () => {
+    const Comp = {
+      render() {
+        return h(Suspense, null, {
+          default: h('div', [
+            h(RejectingAsync),
+            h(Suspense, null, {
+              default: h('div', [h(ResolvingAsync)]),
+              fallback: h('div', 'fallback 2')
+            })
+          ]),
+          fallback: h('div', 'fallback 1')
+        })
+      }
+    }
+
+    expect(await renderToString(createApp(Comp))).toBe(`<div>fallback 1</div>`)
+  })
+})

+ 19 - 2
packages/server-renderer/src/renderToString.ts

@@ -36,7 +36,8 @@ const {
   setCurrentRenderingInstance,
   setupComponent,
   renderComponentRoot,
-  normalizeVNode
+  normalizeVNode,
+  normalizeSuspenseChildren
 } = ssrUtils
 
 // Each component has a buffer array.
@@ -248,7 +249,7 @@ function renderVNode(
       } else if (shapeFlag & ShapeFlags.PORTAL) {
         renderPortal(vnode, parentComponent)
       } else if (shapeFlag & ShapeFlags.SUSPENSE) {
-        // TODO
+        push(renderSuspense(vnode, parentComponent))
       } else {
         console.warn(
           '[@vue/server-renderer] Invalid VNode type:',
@@ -365,3 +366,19 @@ async function resolvePortals(context: SSRContext) {
     }
   }
 }
+
+async function renderSuspense(
+  vnode: VNode,
+  parentComponent: ComponentInternalInstance
+): Promise<ResolvedSSRBuffer> {
+  const { content, fallback } = normalizeSuspenseChildren(vnode)
+  try {
+    const { push, getBuffer } = createBuffer()
+    renderVNode(push, content, parentComponent)
+    return await getBuffer()
+  } catch {
+    const { push, getBuffer } = createBuffer()
+    renderVNode(push, fallback, parentComponent)
+    return getBuffer()
+  }
+}