From 9309b044bd4f9d0a34e0d702ed4690a529443a41 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 12 May 2022 15:06:32 +0800 Subject: [PATCH] fix(ssr): fix hydration error for slot outlet inside transition fix #3989 --- .../__tests__/ssrSlotOutlet.spec.ts | 13 ++++++++ packages/compiler-ssr/src/runtimeHelpers.ts | 2 ++ .../src/transforms/ssrTransformSlotOutlet.ts | 30 +++++++++++++++---- packages/runtime-core/src/hydration.ts | 12 ++++---- .../src/helpers/ssrRenderSlot.ts | 22 +++++++++++++- packages/server-renderer/src/index.ts | 2 +- 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts index d4c6fee9..695cfdf7 100644 --- a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts @@ -1,4 +1,5 @@ import { compile } from '../src' +import { ssrHelpers, SSR_RENDER_SLOT_INNER } from '../src/runtimeHelpers' describe('ssr: ', () => { test('basic', () => { @@ -114,4 +115,16 @@ describe('ssr: ', () => { }" `) }) + + test('inside transition', () => { + const { code } = compile(``) + 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) + }" + `) + }) }) diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts index f0a6a2f2..220af485 100644 --- a/packages/compiler-ssr/src/runtimeHelpers.ts +++ b/packages/compiler-ssr/src/runtimeHelpers.ts @@ -4,6 +4,7 @@ export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`) export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`) export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`) 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_STYLE = Symbol(`ssrRenderStyle`) export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`) @@ -24,6 +25,7 @@ export const ssrHelpers = { [SSR_RENDER_VNODE]: `ssrRenderVNode`, [SSR_RENDER_COMPONENT]: `ssrRenderComponent`, [SSR_RENDER_SLOT]: `ssrRenderSlot`, + [SSR_RENDER_SLOT_INNER]: `ssrRenderSlotInner`, [SSR_RENDER_CLASS]: `ssrRenderClass`, [SSR_RENDER_STYLE]: `ssrRenderStyle`, [SSR_RENDER_ATTRS]: `ssrRenderAttrs`, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts index 98020f6c..21c33831 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts @@ -4,9 +4,13 @@ import { processSlotOutlet, createCallExpression, SlotOutletNode, - createFunctionExpression + createFunctionExpression, + NodeTypes, + ElementTypes, + resolveComponentType, + TRANSITION } from '@vue/compiler-dom' -import { SSR_RENDER_SLOT } from '../runtimeHelpers' +import { SSR_RENDER_SLOT, SSR_RENDER_SLOT_INNER } from '../runtimeHelpers' import { SSRTransformContext, processChildrenAsStatement @@ -31,10 +35,24 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => { args.push(`"${context.scopeId}-s"`) } - node.ssrCodegenNode = createCallExpression( - context.helper(SSR_RENDER_SLOT), - args - ) + let method = SSR_RENDER_SLOT + + // #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) } } diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 3d86f0f2..ae0415aa 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -113,7 +113,7 @@ export function createHydrationFunctions( nextNode = onMismatch() } else { if ((node as Text).data !== vnode.children) { - hasMismatch = true + hasMismatch = true; debugger __DEV__ && warn( `Hydration text mismatch:` + @@ -351,7 +351,7 @@ export function createHydrationFunctions( ) let hasWarned = false while (next) { - hasMismatch = true + hasMismatch = true; debugger if (__DEV__ && !hasWarned) { warn( `Hydration children mismatch in <${vnode.type as string}>: ` + @@ -366,7 +366,7 @@ export function createHydrationFunctions( } } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { if (el.textContent !== vnode.children) { - hasMismatch = true + hasMismatch = true; debugger __DEV__ && warn( `Hydration text content mismatch in <${ @@ -411,7 +411,7 @@ export function createHydrationFunctions( } else if (vnode.type === Text && !vnode.children) { continue } else { - hasMismatch = true + hasMismatch = true; debugger if (__DEV__ && !hasWarned) { warn( `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` + @@ -465,7 +465,7 @@ export function createHydrationFunctions( } else { // fragment didn't hydrate successfully, since we didn't get a end anchor // 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 insert((vnode.anchor = createComment(`]`)), container, next) return next @@ -480,7 +480,7 @@ export function createHydrationFunctions( slotScopeIds: string[] | null, isFragment: boolean ): Node | null => { - hasMismatch = true + hasMismatch = true; debugger __DEV__ && warn( `Hydration node mismatch:\n- Client vnode:`, diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts index 8b9a3971..967b2031 100644 --- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts +++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts @@ -21,6 +21,27 @@ export function ssrRenderSlot( ) { // template-compiled slots are always rendered as fragments 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] if (slotFn) { const slotBuffer: SSRBufferItem[] = [] @@ -59,7 +80,6 @@ export function ssrRenderSlot( } else if (fallbackRenderFn) { fallbackRenderFn() } - push(``) } const commentRE = /^$/ diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index e8b716a8..b6d973ea 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -18,7 +18,7 @@ export { // internal runtime helpers export { renderVNode as ssrRenderVNode } from './render' export { ssrRenderComponent } from './helpers/ssrRenderComponent' -export { ssrRenderSlot } from './helpers/ssrRenderSlot' +export { ssrRenderSlot, ssrRenderSlotInner } from './helpers/ssrRenderSlot' export { ssrRenderTeleport } from './helpers/ssrRenderTeleport' export { ssrRenderClass,