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