feat(ssr/suspense): suspense hydration
In order to support hydration of async components, server-rendered fragments must be explicitly marked with comment nodes.
This commit is contained in:
@@ -257,7 +257,7 @@ describe('ssr: renderToString', () => {
|
||||
)
|
||||
).toBe(
|
||||
`<div>parent<div class="child">` +
|
||||
`<span>from slot</span>` +
|
||||
`<!--1--><span>from slot</span><!--0-->` +
|
||||
`</div></div>`
|
||||
)
|
||||
|
||||
@@ -272,7 +272,9 @@ describe('ssr: renderToString', () => {
|
||||
}
|
||||
})
|
||||
)
|
||||
).toBe(`<div>parent<div class="child">fallback</div></div>`)
|
||||
).toBe(
|
||||
`<div>parent<div class="child"><!--1-->fallback<!--0--></div></div>`
|
||||
)
|
||||
})
|
||||
|
||||
test('nested components with vnode slots', async () => {
|
||||
@@ -316,7 +318,7 @@ describe('ssr: renderToString', () => {
|
||||
)
|
||||
).toBe(
|
||||
`<div>parent<div class="child">` +
|
||||
`<span>from slot</span>` +
|
||||
`<!--1--><span>from slot</span><!--0-->` +
|
||||
`</div></div>`
|
||||
)
|
||||
})
|
||||
@@ -328,13 +330,13 @@ describe('ssr: renderToString', () => {
|
||||
}
|
||||
|
||||
const app = createApp({
|
||||
components: { Child },
|
||||
template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
|
||||
})
|
||||
app.component('Child', Child)
|
||||
|
||||
expect(await renderToString(app)).toBe(
|
||||
`<div>parent<div class="child">` +
|
||||
`<span>from slot</span>` +
|
||||
`<!--1--><span>from slot</span><!--0-->` +
|
||||
`</div></div>`
|
||||
)
|
||||
})
|
||||
@@ -360,6 +362,7 @@ describe('ssr: renderToString', () => {
|
||||
|
||||
expect(await renderToString(app)).toBe(
|
||||
`<div>parent<div class="child">` +
|
||||
// no comment anchors because slot is used directly as element children
|
||||
`<span>from slot</span>` +
|
||||
`</div></div>`
|
||||
)
|
||||
@@ -456,7 +459,9 @@ describe('ssr: renderToString', () => {
|
||||
createCommentVNode('qux')
|
||||
])
|
||||
)
|
||||
).toBe(`<div>foo<span>bar</span><span>baz</span><!--qux--></div>`)
|
||||
).toBe(
|
||||
`<div>foo<span>bar</span><!--1--><span>baz</span><!--0--><!--qux--></div>`
|
||||
)
|
||||
})
|
||||
|
||||
test('void elements', async () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('SSR Suspense', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(await renderToString(app)).toBe(`<div>async</div>`)
|
||||
expect(await renderToString(app)).toBe(`<!--1--><div>async</div><!--0-->`)
|
||||
})
|
||||
|
||||
test('with async component', async () => {
|
||||
@@ -49,7 +49,7 @@ describe('SSR Suspense', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(await renderToString(app)).toBe(`<div>async</div>`)
|
||||
expect(await renderToString(app)).toBe(`<!--1--><div>async</div><!--0-->`)
|
||||
})
|
||||
|
||||
test('fallback', async () => {
|
||||
@@ -68,7 +68,9 @@ describe('SSR Suspense', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(await renderToString(app)).toBe(`<div>fallback</div>`)
|
||||
expect(await renderToString(app)).toBe(
|
||||
`<!--1--><div>fallback</div><!--0-->`
|
||||
)
|
||||
expect('Uncaught error in async setup').toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,6 +18,8 @@ export function ssrRenderSlot(
|
||||
push: PushFn,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
// template-compiled slots are always rendered as fragments
|
||||
push(`<!--1-->`)
|
||||
const slotFn = slots[slotName]
|
||||
if (slotFn) {
|
||||
if (slotFn.length > 1) {
|
||||
@@ -31,4 +33,5 @@ export function ssrRenderSlot(
|
||||
} else if (fallbackRenderFn) {
|
||||
fallbackRenderFn()
|
||||
}
|
||||
push(`<!--0-->`)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import { PushFn, ResolvedSSRBuffer, createBuffer } from '../renderToString'
|
||||
import { NOOP } from '@vue/shared'
|
||||
|
||||
type ContentRenderFn = (push: PushFn) => void
|
||||
|
||||
export async function ssrRenderSuspense({
|
||||
default: renderContent = NOOP,
|
||||
fallback: renderFallback = NOOP
|
||||
default: renderContent,
|
||||
fallback: renderFallback
|
||||
}: Record<string, ContentRenderFn | undefined>): Promise<ResolvedSSRBuffer> {
|
||||
try {
|
||||
const { push, getBuffer } = createBuffer()
|
||||
renderContent(push)
|
||||
return await getBuffer()
|
||||
if (renderContent) {
|
||||
const { push, getBuffer } = createBuffer()
|
||||
push(`<!--1-->`)
|
||||
renderContent(push)
|
||||
push(`<!--0-->`)
|
||||
return await getBuffer()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} catch {
|
||||
const { push, getBuffer } = createBuffer()
|
||||
renderFallback(push)
|
||||
return getBuffer()
|
||||
if (renderFallback) {
|
||||
const { push, getBuffer } = createBuffer()
|
||||
push(`<!--1-->`)
|
||||
renderFallback(push)
|
||||
push(`<!--0-->`)
|
||||
return getBuffer()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,9 @@ function renderVNode(
|
||||
push(children ? `<!--${children}-->` : `<!---->`)
|
||||
break
|
||||
case Fragment:
|
||||
push(`<!--1-->`) // open
|
||||
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
|
||||
push(`<!--0-->`) // close
|
||||
break
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
|
||||
Reference in New Issue
Block a user