fix(ssr): fix hydration error for slot outlet inside transition

fix #3989
This commit is contained in:
Evan You 2022-05-12 15:06:32 +08:00
parent da49c863a2
commit 9309b044bd
6 changed files with 67 additions and 14 deletions

View File

@ -1,4 +1,5 @@
import { compile } from '../src' import { compile } from '../src'
import { ssrHelpers, SSR_RENDER_SLOT_INNER } from '../src/runtimeHelpers'
describe('ssr: <slot>', () => { describe('ssr: <slot>', () => {
test('basic', () => { test('basic', () => {
@ -114,4 +115,16 @@ describe('ssr: <slot>', () => {
}" }"
`) `)
}) })
test('inside transition', () => {
const { code } = compile(`<transition><slot/></transition>`)
expect(code).toMatch(ssrHelpers[SSR_RENDER_SLOT_INNER])
expect(code).toMatchInlineSnapshot(`
"const { ssrRenderSlotInner: _ssrRenderSlotInner } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_ssrRenderSlotInner(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
}"
`)
})
}) })

View File

@ -4,6 +4,7 @@ export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`)
export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`) export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`)
export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`) export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`)
export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`) export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`)
export const SSR_RENDER_SLOT_INNER = Symbol(`ssrRenderSlotInner`)
export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`) export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`)
export const SSR_RENDER_STYLE = Symbol(`ssrRenderStyle`) export const SSR_RENDER_STYLE = Symbol(`ssrRenderStyle`)
export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`) export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
@ -24,6 +25,7 @@ export const ssrHelpers = {
[SSR_RENDER_VNODE]: `ssrRenderVNode`, [SSR_RENDER_VNODE]: `ssrRenderVNode`,
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`, [SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
[SSR_RENDER_SLOT]: `ssrRenderSlot`, [SSR_RENDER_SLOT]: `ssrRenderSlot`,
[SSR_RENDER_SLOT_INNER]: `ssrRenderSlotInner`,
[SSR_RENDER_CLASS]: `ssrRenderClass`, [SSR_RENDER_CLASS]: `ssrRenderClass`,
[SSR_RENDER_STYLE]: `ssrRenderStyle`, [SSR_RENDER_STYLE]: `ssrRenderStyle`,
[SSR_RENDER_ATTRS]: `ssrRenderAttrs`, [SSR_RENDER_ATTRS]: `ssrRenderAttrs`,

View File

@ -4,9 +4,13 @@ import {
processSlotOutlet, processSlotOutlet,
createCallExpression, createCallExpression,
SlotOutletNode, SlotOutletNode,
createFunctionExpression createFunctionExpression,
NodeTypes,
ElementTypes,
resolveComponentType,
TRANSITION
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SSR_RENDER_SLOT } from '../runtimeHelpers' import { SSR_RENDER_SLOT, SSR_RENDER_SLOT_INNER } from '../runtimeHelpers'
import { import {
SSRTransformContext, SSRTransformContext,
processChildrenAsStatement processChildrenAsStatement
@ -31,10 +35,24 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
args.push(`"${context.scopeId}-s"`) args.push(`"${context.scopeId}-s"`)
} }
node.ssrCodegenNode = createCallExpression( let method = SSR_RENDER_SLOT
context.helper(SSR_RENDER_SLOT),
args // #3989
) // check if this is a single slot inside a transition wrapper - since
// transition will unwrap the slot fragment into a single vnode at runtime,
// we need to avoid rendering the slot as a fragment.
const parent = context.parent
if (
parent &&
parent.type === NodeTypes.ELEMENT &&
parent.tagType === ElementTypes.COMPONENT &&
resolveComponentType(parent, context, true) === TRANSITION &&
parent.children.filter(c => c.type === NodeTypes.ELEMENT).length === 1
) {
method = SSR_RENDER_SLOT_INNER
}
node.ssrCodegenNode = createCallExpression(context.helper(method), args)
} }
} }

View File

@ -113,7 +113,7 @@ export function createHydrationFunctions(
nextNode = onMismatch() nextNode = onMismatch()
} else { } else {
if ((node as Text).data !== vnode.children) { if ((node as Text).data !== vnode.children) {
hasMismatch = true hasMismatch = true; debugger
__DEV__ && __DEV__ &&
warn( warn(
`Hydration text mismatch:` + `Hydration text mismatch:` +
@ -351,7 +351,7 @@ export function createHydrationFunctions(
) )
let hasWarned = false let hasWarned = false
while (next) { while (next) {
hasMismatch = true hasMismatch = true; debugger
if (__DEV__ && !hasWarned) { if (__DEV__ && !hasWarned) {
warn( warn(
`Hydration children mismatch in <${vnode.type as string}>: ` + `Hydration children mismatch in <${vnode.type as string}>: ` +
@ -366,7 +366,7 @@ export function createHydrationFunctions(
} }
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (el.textContent !== vnode.children) { if (el.textContent !== vnode.children) {
hasMismatch = true hasMismatch = true; debugger
__DEV__ && __DEV__ &&
warn( warn(
`Hydration text content mismatch in <${ `Hydration text content mismatch in <${
@ -411,7 +411,7 @@ export function createHydrationFunctions(
} else if (vnode.type === Text && !vnode.children) { } else if (vnode.type === Text && !vnode.children) {
continue continue
} else { } else {
hasMismatch = true hasMismatch = true; debugger
if (__DEV__ && !hasWarned) { if (__DEV__ && !hasWarned) {
warn( warn(
`Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` + `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
@ -465,7 +465,7 @@ export function createHydrationFunctions(
} else { } else {
// fragment didn't hydrate successfully, since we didn't get a end anchor // fragment didn't hydrate successfully, since we didn't get a end anchor
// back. This should have led to node/children mismatch warnings. // back. This should have led to node/children mismatch warnings.
hasMismatch = true hasMismatch = true; debugger
// since the anchor is missing, we need to create one and insert it // since the anchor is missing, we need to create one and insert it
insert((vnode.anchor = createComment(`]`)), container, next) insert((vnode.anchor = createComment(`]`)), container, next)
return next return next
@ -480,7 +480,7 @@ export function createHydrationFunctions(
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
isFragment: boolean isFragment: boolean
): Node | null => { ): Node | null => {
hasMismatch = true hasMismatch = true; debugger
__DEV__ && __DEV__ &&
warn( warn(
`Hydration node mismatch:\n- Client vnode:`, `Hydration node mismatch:\n- Client vnode:`,

View File

@ -21,6 +21,27 @@ export function ssrRenderSlot(
) { ) {
// template-compiled slots are always rendered as fragments // template-compiled slots are always rendered as fragments
push(`<!--[-->`) push(`<!--[-->`)
ssrRenderSlotInner(
slots,
slotName,
slotProps,
fallbackRenderFn,
push,
parentComponent,
slotScopeId
)
push(`<!--]-->`)
}
export function ssrRenderSlotInner(
slots: Slots | SSRSlots,
slotName: string,
slotProps: Props,
fallbackRenderFn: (() => void) | null,
push: PushFn,
parentComponent: ComponentInternalInstance,
slotScopeId?: string
) {
const slotFn = slots[slotName] const slotFn = slots[slotName]
if (slotFn) { if (slotFn) {
const slotBuffer: SSRBufferItem[] = [] const slotBuffer: SSRBufferItem[] = []
@ -59,7 +80,6 @@ export function ssrRenderSlot(
} else if (fallbackRenderFn) { } else if (fallbackRenderFn) {
fallbackRenderFn() fallbackRenderFn()
} }
push(`<!--]-->`)
} }
const commentRE = /^<!--.*-->$/ const commentRE = /^<!--.*-->$/

View File

@ -18,7 +18,7 @@ export {
// internal runtime helpers // internal runtime helpers
export { renderVNode as ssrRenderVNode } from './render' export { renderVNode as ssrRenderVNode } from './render'
export { ssrRenderComponent } from './helpers/ssrRenderComponent' export { ssrRenderComponent } from './helpers/ssrRenderComponent'
export { ssrRenderSlot } from './helpers/ssrRenderSlot' export { ssrRenderSlot, ssrRenderSlotInner } from './helpers/ssrRenderSlot'
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport' export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
export { export {
ssrRenderClass, ssrRenderClass,