diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index b3a6b31c6d4..a783502fc77 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -449,7 +449,7 @@ function createSuspenseBoundary( return suspense } -function normalizeSuspenseChildren( +export function normalizeSuspenseChildren( vnode: VNode ): { content: VNode diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index d0b8444a8fd..9717174bbe3 100644 --- a/packages/runtime-core/src/index.ts +++ b/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 diff --git a/packages/server-renderer/__tests__/ssrSuspense.spec.ts b/packages/server-renderer/__tests__/ssrSuspense.spec.ts new file mode 100644 index 00000000000..b411e14a780 --- /dev/null +++ b/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(`
async
`) + }) + + test('fallback', async () => { + const Comp = { + render() { + return h(Suspense, null, { + default: h(RejectingAsync), + fallback: h('div', 'fallback') + }) + } + } + + expect(await renderToString(createApp(Comp))).toBe(`
fallback
`) + }) + + 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( + `
async
async
` + ) + }) + + 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(`
fallback
`) + }) + + 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( + `
async
fallback 2
` + ) + }) + + 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(`
fallback 1
`) + }) +}) diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index b181b5422cb..8051c204b4f 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/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 { + 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() + } +}