feat(server-renderer): render suspense in vnode mode (#727)
This commit is contained in:
		
							parent
							
								
									e12ddd96ba
								
							
						
					
					
						commit
						589aeb402c
					
				@ -449,7 +449,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
 | 
				
			|||||||
  return suspense
 | 
					  return suspense
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function normalizeSuspenseChildren(
 | 
					export function normalizeSuspenseChildren(
 | 
				
			||||||
  vnode: VNode
 | 
					  vnode: VNode
 | 
				
			||||||
): {
 | 
					): {
 | 
				
			||||||
  content: VNode
 | 
					  content: VNode
 | 
				
			||||||
 | 
				
			|||||||
@ -114,6 +114,7 @@ import {
 | 
				
			|||||||
  setCurrentRenderingInstance
 | 
					  setCurrentRenderingInstance
 | 
				
			||||||
} from './componentRenderUtils'
 | 
					} from './componentRenderUtils'
 | 
				
			||||||
import { isVNode, normalizeVNode } from './vnode'
 | 
					import { isVNode, normalizeVNode } from './vnode'
 | 
				
			||||||
 | 
					import { normalizeSuspenseChildren } from './components/Suspense'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SSR utils are only exposed in cjs builds.
 | 
					// SSR utils are only exposed in cjs builds.
 | 
				
			||||||
const _ssrUtils = {
 | 
					const _ssrUtils = {
 | 
				
			||||||
@ -122,7 +123,8 @@ const _ssrUtils = {
 | 
				
			|||||||
  renderComponentRoot,
 | 
					  renderComponentRoot,
 | 
				
			||||||
  setCurrentRenderingInstance,
 | 
					  setCurrentRenderingInstance,
 | 
				
			||||||
  isVNode,
 | 
					  isVNode,
 | 
				
			||||||
  normalizeVNode
 | 
					  normalizeVNode,
 | 
				
			||||||
 | 
					  normalizeSuspenseChildren
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ssrUtils = (__NODE_JS__ ? _ssrUtils : null) as typeof _ssrUtils
 | 
					export const ssrUtils = (__NODE_JS__ ? _ssrUtils : null) as typeof _ssrUtils
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										110
									
								
								packages/server-renderer/__tests__/ssrSuspense.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								packages/server-renderer/__tests__/ssrSuspense.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -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>`)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
@ -36,7 +36,8 @@ const {
 | 
				
			|||||||
  setCurrentRenderingInstance,
 | 
					  setCurrentRenderingInstance,
 | 
				
			||||||
  setupComponent,
 | 
					  setupComponent,
 | 
				
			||||||
  renderComponentRoot,
 | 
					  renderComponentRoot,
 | 
				
			||||||
  normalizeVNode
 | 
					  normalizeVNode,
 | 
				
			||||||
 | 
					  normalizeSuspenseChildren
 | 
				
			||||||
} = ssrUtils
 | 
					} = ssrUtils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Each component has a buffer array.
 | 
					// Each component has a buffer array.
 | 
				
			||||||
@ -248,7 +249,7 @@ function renderVNode(
 | 
				
			|||||||
      } else if (shapeFlag & ShapeFlags.PORTAL) {
 | 
					      } else if (shapeFlag & ShapeFlags.PORTAL) {
 | 
				
			||||||
        renderPortal(vnode, parentComponent)
 | 
					        renderPortal(vnode, parentComponent)
 | 
				
			||||||
      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
 | 
					      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
 | 
				
			||||||
        // TODO
 | 
					        push(renderSuspense(vnode, parentComponent))
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        console.warn(
 | 
					        console.warn(
 | 
				
			||||||
          '[@vue/server-renderer] Invalid VNode type:',
 | 
					          '[@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()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user