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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function normalizeSuspenseChildren(
 | 
			
		||||
export function normalizeSuspenseChildren(
 | 
			
		||||
  vnode: VNode
 | 
			
		||||
): {
 | 
			
		||||
  content: VNode
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
								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,
 | 
			
		||||
  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()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user