]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(server-renderer): render suspense in vnode mode (#727)
authorDmitry Sharshakov <d3dx12.xx@gmail.com>
Mon, 9 Mar 2020 22:20:30 +0000 (01:20 +0300)
committerGitHub <noreply@github.com>
Mon, 9 Mar 2020 22:20:30 +0000 (18:20 -0400)
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/index.ts
packages/server-renderer/__tests__/ssrSuspense.spec.ts [new file with mode: 0644]
packages/server-renderer/src/renderToString.ts

index b3a6b31c6d4387670c0bb3c5e4febab11bb0cb4b..a783502fc77782b7d2d8523856026ba7419e3cd9 100644 (file)
@@ -449,7 +449,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
   return suspense
 }
 
-function normalizeSuspenseChildren(
+export function normalizeSuspenseChildren(
   vnode: VNode
 ): {
   content: VNode
index d0b8444a8fde7189be5cd594894711e920a24768..9717174bbe3ecba879574c140bf06e23d252974a 100644 (file)
@@ -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
diff --git a/packages/server-renderer/__tests__/ssrSuspense.spec.ts b/packages/server-renderer/__tests__/ssrSuspense.spec.ts
new file mode 100644 (file)
index 0000000..b411e14
--- /dev/null
@@ -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>`)
+  })
+})
index b181b5422cb31b65c84449e73f2d86f1e24aa212..8051c204b4fd9ea30806373f94545fc367cf7fa4 100644 (file)
@@ -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()
+  }
+}