From 5ea8a8a4fab4e19a71e123e4d27d051f5e927172 Mon Sep 17 00:00:00 2001 From: edison Date: Tue, 24 Oct 2023 09:36:10 +0800 Subject: [PATCH] fix(transition/ssr): make transition appear work with SSR (#8859) close #6951 --- .../__tests__/ssrTransition.spec.ts | 25 +++++ .../src/transforms/ssrTransformComponent.ts | 12 ++- .../src/transforms/ssrTransformTransition.ts | 36 +++++++ .../runtime-core/__tests__/hydration.spec.ts | 76 ++++++++++++- packages/runtime-core/src/hydration.ts | 101 +++++++++++++++--- packages/runtime-core/src/renderer.ts | 17 ++- 6 files changed, 241 insertions(+), 26 deletions(-) create mode 100644 packages/compiler-ssr/__tests__/ssrTransition.spec.ts create mode 100644 packages/compiler-ssr/src/transforms/ssrTransformTransition.ts diff --git a/packages/compiler-ssr/__tests__/ssrTransition.spec.ts b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts new file mode 100644 index 00000000000..319b3902239 --- /dev/null +++ b/packages/compiler-ssr/__tests__/ssrTransition.spec.ts @@ -0,0 +1,25 @@ +import { compile } from '../src' + +describe('transition', () => { + test('basic', () => { + expect(compile(`
foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`foo\`) + }" + `) + }) + + test('with appear', () => { + expect(compile(`
foo
`).code) + .toMatchInlineSnapshot(` + "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`\`) + }" + `) + }) +}) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index dc8c6a4ae4f..93cae7db3c2 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -56,6 +56,10 @@ import { } from './ssrTransformTransitionGroup' import { isSymbol, isObject, isArray } from '@vue/shared' import { buildSSRProps } from './ssrTransformElement' +import { + ssrProcessTransition, + ssrTransformTransition +} from './ssrTransformTransition' // We need to construct the slot functions in the 1st pass to ensure proper // scope tracking, but the children of each slot cannot be processed until @@ -99,9 +103,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { if (isSymbol(component)) { if (component === SUSPENSE) { return ssrTransformSuspense(node, context) - } - if (component === TRANSITION_GROUP) { + } else if (component === TRANSITION_GROUP) { return ssrTransformTransitionGroup(node, context) + } else if (component === TRANSITION) { + return ssrTransformTransition(node, context) } return // other built-in components: fallthrough } @@ -216,9 +221,8 @@ export function ssrProcessComponent( if ((parent as WIPSlotEntry).type === WIP_SLOT) { context.pushStringPart(``) } - // #5351: filter out comment children inside transition if (component === TRANSITION) { - node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + return ssrProcessTransition(node, context) } processChildren(node, context) } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts new file mode 100644 index 00000000000..d09a806f7b0 --- /dev/null +++ b/packages/compiler-ssr/src/transforms/ssrTransformTransition.ts @@ -0,0 +1,36 @@ +import { + ComponentNode, + findProp, + NodeTypes, + TransformContext +} from '@vue/compiler-dom' +import { processChildren, SSRTransformContext } from '../ssrCodegenTransform' + +const wipMap = new WeakMap() + +export function ssrTransformTransition( + node: ComponentNode, + context: TransformContext +) { + return () => { + const appear = findProp(node, 'appear', false, true) + wipMap.set(node, !!appear) + } +} + +export function ssrProcessTransition( + node: ComponentNode, + context: SSRTransformContext +) { + // #5351: filter out comment children inside transition + node.children = node.children.filter(c => c.type !== NodeTypes.COMMENT) + + const appear = wipMap.get(node) + if (appear) { + context.pushStringPart(``) + } else { + processChildren(node, context, false, true) + } +} diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index f0a3a9333a7..759804b97f1 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -18,10 +18,14 @@ import { createVNode, withDirectives, vModelCheckbox, - renderSlot + renderSlot, + Transition, + createCommentVNode, + vShow } from '@vue/runtime-dom' import { renderToString, SSRContext } from '@vue/server-renderer' -import { PatchFlags } from '../../shared/src' +import { PatchFlags } from '@vue/shared' +import { vShowOldKey } from '../../runtime-dom/src/directives/vShow' function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') @@ -1016,6 +1020,74 @@ describe('SSR hydration', () => { expect(`mismatch`).not.toHaveBeenWarned() }) + test('transition appear', () => { + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => h('div', 'foo') + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` +
+ foo +
+ `) + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-if', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => (show ? h('div', 'foo') : createCommentVNode('')) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + + test('transition appear with v-show', () => { + const show = false + const { vnode, container } = mountWithHydration( + ``, + () => + h( + Transition, + { appear: true }, + { + default: () => + withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]) + } + ) + ) + expect(container.firstChild).toMatchInlineSnapshot(` + + `) + expect((container.firstChild as any)[vShowOldKey]).toBe('') + expect(vnode.el).toBe(container.firstChild) + expect(`mismatch`).not.toHaveBeenWarned() + }) + describe('mismatch handling', () => { test('text node', () => { const { container } = mountWithHydration(`foo`, () => 'bar') diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 097443dbc53..4e91cb3d1cb 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -15,7 +15,7 @@ import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' -import { RendererInternals } from './renderer' +import { needTransition, RendererInternals } from './renderer' import { setRef } from './rendererTemplateRef' import { SuspenseImpl, @@ -146,7 +146,17 @@ export function createHydrationFunctions( break case Comment: if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) { - nextNode = onMismatch() + if ((node as Element).tagName.toLowerCase() === 'template') { + const content = (vnode.el! as HTMLTemplateElement).content + .firstChild! + + // replace