From 7b18cdb0b53a94007ca6a3675bf41b5d3153fec6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 31 Jul 2024 16:09:56 +0800 Subject: [PATCH] fix(teleport/ssr): fix Teleport hydration regression due to targetStart anchor addition --- .../runtime-core/__tests__/hydration.spec.ts | 100 +++++++++++++++--- .../runtime-core/src/components/Teleport.ts | 32 +++--- .../__tests__/ssrTeleport.spec.ts | 19 ++-- .../src/helpers/ssrRenderTeleport.ts | 3 +- 4 files changed, 114 insertions(+), 40 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 9916fafa62c..c525618b510 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -265,7 +265,7 @@ describe('SSR hydration', () => { const fn = vi.fn() const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport' - teleportContainer.innerHTML = `foo` + teleportContainer.innerHTML = `foo` document.body.appendChild(teleportContainer) const { vnode, container } = mountWithHydration( @@ -281,13 +281,14 @@ describe('SSR hydration', () => { expect(vnode.anchor).toBe(container.lastChild) expect(vnode.target).toBe(teleportContainer) + expect(vnode.targetStart).toBe(teleportContainer.childNodes[0]) expect((vnode.children as VNode[])[0].el).toBe( - teleportContainer.childNodes[0], + teleportContainer.childNodes[1], ) expect((vnode.children as VNode[])[1].el).toBe( - teleportContainer.childNodes[1], + teleportContainer.childNodes[2], ) - expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2]) + expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3]) // event handler triggerEvent('click', teleportContainer.querySelector('.foo')!) @@ -296,7 +297,7 @@ describe('SSR hydration', () => { msg.value = 'bar' await nextTick() expect(teleportContainer.innerHTML).toBe( - `bar`, + `bar`, ) }) @@ -326,7 +327,7 @@ describe('SSR hydration', () => { const teleportHtml = ctx.teleports!['#teleport2'] expect(teleportHtml).toMatchInlineSnapshot( - `"foofoo2"`, + `"foofoo2"`, ) teleportContainer.innerHTML = teleportHtml @@ -342,16 +343,18 @@ describe('SSR hydration', () => { expect(teleportVnode2.anchor).toBe(container.childNodes[4]) expect(teleportVnode1.target).toBe(teleportContainer) + expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0]) expect((teleportVnode1 as any).children[0].el).toBe( - teleportContainer.childNodes[0], + teleportContainer.childNodes[1], ) - expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2]) + expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3]) expect(teleportVnode2.target).toBe(teleportContainer) + expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4]) expect((teleportVnode2 as any).children[0].el).toBe( - teleportContainer.childNodes[3], + teleportContainer.childNodes[5], ) - expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5]) + expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7]) // // event handler triggerEvent('click', teleportContainer.querySelector('.foo')!) @@ -363,7 +366,7 @@ describe('SSR hydration', () => { msg.value = 'bar' await nextTick() expect(teleportContainer.innerHTML).toMatchInlineSnapshot( - `"barbar2"`, + `"barbar2"`, ) }) @@ -390,7 +393,9 @@ describe('SSR hydration', () => { ) const teleportHtml = ctx.teleports!['#teleport3'] - expect(teleportHtml).toMatchInlineSnapshot(`""`) + expect(teleportHtml).toMatchInlineSnapshot( + `""`, + ) teleportContainer.innerHTML = teleportHtml document.body.appendChild(teleportContainer) @@ -413,7 +418,8 @@ describe('SSR hydration', () => { expect(children[2].el).toBe(container.childNodes[6]) expect(teleportVnode.target).toBe(teleportContainer) - expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0]) + expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0]) + expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1]) // // event handler triggerEvent('click', container.querySelector('.foo')!) @@ -454,7 +460,7 @@ describe('SSR hydration', () => { test('Teleport (as component root)', () => { const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport4' - teleportContainer.innerHTML = `hello` + teleportContainer.innerHTML = `hello` document.body.appendChild(teleportContainer) const wrapper = { @@ -483,7 +489,7 @@ describe('SSR hydration', () => { test('Teleport (nested)', () => { const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport5' - teleportContainer.innerHTML = `
child
` + teleportContainer.innerHTML = `
child
` document.body.appendChild(teleportContainer) const { vnode, container } = mountWithHydration( @@ -498,7 +504,7 @@ describe('SSR hydration', () => { expect(vnode.anchor).toBe(container.lastChild) const childDivVNode = (vnode as any).children[0] - const div = teleportContainer.firstChild + const div = teleportContainer.childNodes[1] expect(childDivVNode.el).toBe(div) expect(vnode.targetAnchor).toBe(div?.nextSibling) @@ -548,6 +554,66 @@ describe('SSR hydration', () => { teleportContainer.id = 'target' document.body.appendChild(teleportContainer) + // server render + const ctx: SSRContext = {} + container.innerHTML = await renderToString(h(App), ctx) + expect(container.innerHTML).toBe( + '
', + ) + teleportContainer.innerHTML = ctx.teleports!['#target'] + + // hydrate + createSSRApp(App).mount(container) + expect(container.innerHTML).toBe( + '
', + ) + expect(teleportContainer.innerHTML).toBe( + 'Teleported Comp1', + ) + expect(`Hydration children mismatch`).not.toHaveBeenWarned() + + toggle.value = false + await nextTick() + expect(container.innerHTML).toBe('
Comp2
') + expect(teleportContainer.innerHTML).toBe('') + }) + + test('Teleport unmount (mismatch + full integration)', async () => { + const Comp1 = { + template: ` + + Teleported Comp1 + + `, + } + const Comp2 = { + template: ` +
Comp2
+ `, + } + + const toggle = ref(true) + const App = { + template: ` +
+ + +
+ `, + components: { + Comp1, + Comp2, + }, + setup() { + return { toggle } + }, + } + + const container = document.createElement('div') + const teleportContainer = document.createElement('div') + teleportContainer.id = 'target' + document.body.appendChild(teleportContainer) + // server render container.innerHTML = await renderToString(h(App)) expect(container.innerHTML).toBe( @@ -569,7 +635,7 @@ describe('SSR hydration', () => { expect(teleportContainer.innerHTML).toBe('') }) - test('Teleport target change (full integration)', async () => { + test('Teleport target change (mismatch + full integration)', async () => { const target = ref('#target1') const Comp = { template: ` diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index 81573cc85a7..d868fbbc669 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -381,7 +381,8 @@ function hydrateTeleport( slotScopeIds, optimized, ) - vnode.targetStart = vnode.targetAnchor = targetNode + vnode.targetStart = targetNode + vnode.targetAnchor = targetNode && nextSibling(targetNode) } else { vnode.anchor = nextSibling(node) @@ -390,28 +391,29 @@ function hydrateTeleport( // could be nested teleports let targetAnchor = targetNode while (targetAnchor) { - targetAnchor = nextSibling(targetAnchor) - if ( - targetAnchor && - targetAnchor.nodeType === 8 && - (targetAnchor as Comment).data === 'teleport anchor' - ) { - vnode.targetAnchor = targetAnchor - ;(target as TeleportTargetElement)._lpa = - vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node) - break + if (targetAnchor && targetAnchor.nodeType === 8) { + if ((targetAnchor as Comment).data === 'teleport start anchor') { + vnode.targetStart = targetAnchor + } else if ((targetAnchor as Comment).data === 'teleport anchor') { + vnode.targetAnchor = targetAnchor + ;(target as TeleportTargetElement)._lpa = + vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node) + break + } } + targetAnchor = nextSibling(targetAnchor) } - // #11400 if the HTML corresponding to Teleport is not embedded in the correct position - // on the final page during SSR. the targetAnchor will always be null, we need to - // manually add targetAnchor to ensure Teleport it can properly unmount or move + // #11400 if the HTML corresponding to Teleport is not embedded in the + // correct position on the final page during SSR. the targetAnchor will + // always be null, we need to manually add targetAnchor to ensure + // Teleport it can properly unmount or move if (!vnode.targetAnchor) { prepareAnchor(target, vnode, createText, insert) } hydrateChildren( - targetNode, + targetNode && nextSibling(targetNode), vnode, target, parentComponent, diff --git a/packages/server-renderer/__tests__/ssrTeleport.spec.ts b/packages/server-renderer/__tests__/ssrTeleport.spec.ts index a0a2f6ae0a1..78c56942636 100644 --- a/packages/server-renderer/__tests__/ssrTeleport.spec.ts +++ b/packages/server-renderer/__tests__/ssrTeleport.spec.ts @@ -28,7 +28,7 @@ describe('ssrRenderTeleport', () => { ) expect(html).toBe('') expect(ctx.teleports!['#target']).toBe( - `
content
`, + `
content
`, ) }) @@ -56,7 +56,9 @@ describe('ssrRenderTeleport', () => { expect(html).toBe( '
content
', ) - expect(ctx.teleports!['#target']).toBe(``) + expect(ctx.teleports!['#target']).toBe( + ``, + ) }) test('teleport rendering (vnode)', async () => { @@ -73,7 +75,7 @@ describe('ssrRenderTeleport', () => { ) expect(html).toBe('') expect(ctx.teleports!['#target']).toBe( - 'hello', + 'hello', ) }) @@ -93,7 +95,9 @@ describe('ssrRenderTeleport', () => { expect(html).toBe( 'hello', ) - expect(ctx.teleports!['#target']).toBe(``) + expect(ctx.teleports!['#target']).toBe( + ``, + ) }) test('multiple teleports with same target', async () => { @@ -115,7 +119,8 @@ describe('ssrRenderTeleport', () => { '
', ) expect(ctx.teleports!['#target']).toBe( - 'helloworld', + 'hello' + + 'world', ) }) @@ -134,7 +139,7 @@ describe('ssrRenderTeleport', () => { ) expect(html).toBe('') expect(ctx.teleports!['#target']).toBe( - `
content
`, + `
content
`, ) }) @@ -169,7 +174,7 @@ describe('ssrRenderTeleport', () => { await p expect(html).toBe('') expect(ctx.teleports!['#target']).toBe( - `
content
`, + `
content
`, ) }) }) diff --git a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts index d83af28c131..0806a3927be 100644 --- a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts +++ b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts @@ -29,9 +29,10 @@ export function ssrRenderTeleport( if (disabled) { contentRenderFn(parentPush) - teleportContent = `` + teleportContent = `` } else { const { getBuffer, push } = createBuffer() + push(``) contentRenderFn(push) push(``) teleportContent = getBuffer()