From 595263c0e9f5728c3650c6526dbed27cda9ba114 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 18 May 2022 18:13:08 +0800 Subject: [PATCH] fix(ssr/teleport): support nested teleports in ssr fix #5242 --- .../runtime-core/__tests__/hydration.spec.ts | 44 ++++++++++++++++--- .../runtime-core/src/components/Teleport.ts | 23 ++++++++-- .../__tests__/ssrTeleport.spec.ts | 22 +++++++--- .../src/helpers/ssrRenderTeleport.ts | 26 +++++------ 4 files changed, 86 insertions(+), 29 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 76e8b15342f..58661233564 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -202,7 +202,7 @@ describe('SSR hydration', () => { const fn = jest.fn() const teleportContainer = document.createElement('div') teleportContainer.id = 'teleport' - teleportContainer.innerHTML = `foo` + teleportContainer.innerHTML = `foo` document.body.appendChild(teleportContainer) const { vnode, container } = mountWithHydration( @@ -233,7 +233,7 @@ describe('SSR hydration', () => { msg.value = 'bar' await nextTick() expect(teleportContainer.innerHTML).toBe( - `bar` + `bar` ) }) @@ -263,7 +263,7 @@ describe('SSR hydration', () => { const teleportHtml = ctx.teleports!['#teleport2'] expect(teleportHtml).toMatchInlineSnapshot( - `"foofoo2"` + `"foofoo2"` ) teleportContainer.innerHTML = teleportHtml @@ -300,7 +300,7 @@ describe('SSR hydration', () => { msg.value = 'bar' await nextTick() expect(teleportContainer.innerHTML).toMatchInlineSnapshot( - `"barbar2"` + `"barbar2"` ) }) @@ -327,7 +327,7 @@ describe('SSR hydration', () => { ) const teleportHtml = ctx.teleports!['#teleport3'] - expect(teleportHtml).toMatchInlineSnapshot(`""`) + expect(teleportHtml).toMatchInlineSnapshot(`""`) teleportContainer.innerHTML = teleportHtml document.body.appendChild(teleportContainer) @@ -369,7 +369,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 = { @@ -395,6 +395,38 @@ describe('SSR hydration', () => { expect(nextVNode.el).toBe(container.firstChild?.lastChild) }) + test('Teleport (nested)', () => { + const teleportContainer = document.createElement('div') + teleportContainer.id = 'teleport5' + teleportContainer.innerHTML = `
child
` + document.body.appendChild(teleportContainer) + + const { vnode, container } = mountWithHydration( + '', + () => + h(Teleport, { to: '#teleport5' }, [ + h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])]) + ]) + ) + + expect(vnode.el).toBe(container.firstChild) + expect(vnode.anchor).toBe(container.lastChild) + + const childDivVNode = (vnode as any).children[0] + const div = teleportContainer.firstChild + expect(childDivVNode.el).toBe(div) + expect(vnode.targetAnchor).toBe(div?.nextSibling) + + const childTeleportVNode = childDivVNode.children[0] + expect(childTeleportVNode.el).toBe(div?.firstChild) + expect(childTeleportVNode.anchor).toBe(div?.lastChild) + + expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild) + expect(childTeleportVNode.children[0].el).toBe( + teleportContainer.lastChild?.previousSibling + ) + }) + // compile SSR + client render fn from the same template & hydrate test('full compiler integration', async () => { const mounted: string[] = [] diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index 68d50a63fbd..06b69aff4ec 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -353,7 +353,26 @@ function hydrateTeleport( vnode.targetAnchor = targetNode } else { vnode.anchor = nextSibling(node) - vnode.targetAnchor = hydrateChildren( + + // lookahead until we find the target anchor + // we cannot rely on return value of hydrateChildren() because there + // 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 + } + } + + hydrateChildren( targetNode, vnode, target, @@ -363,8 +382,6 @@ function hydrateTeleport( optimized ) } - ;(target as TeleportTargetElement)._lpa = - vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node) } } return vnode.anchor && nextSibling(vnode.anchor as Node) diff --git a/packages/server-renderer/__tests__/ssrTeleport.spec.ts b/packages/server-renderer/__tests__/ssrTeleport.spec.ts index af915168b3c..76c5c8eb3d9 100644 --- a/packages/server-renderer/__tests__/ssrTeleport.spec.ts +++ b/packages/server-renderer/__tests__/ssrTeleport.spec.ts @@ -31,7 +31,9 @@ describe('ssrRenderTeleport', () => { ctx ) expect(html).toBe('') - expect(ctx.teleports!['#target']).toBe(`
content
`) + expect(ctx.teleports!['#target']).toBe( + `
content
` + ) }) test('teleport rendering (compiled + disabled)', async () => { @@ -58,7 +60,7 @@ describe('ssrRenderTeleport', () => { expect(html).toBe( '
content
' ) - expect(ctx.teleports!['#target']).toBe(``) + expect(ctx.teleports!['#target']).toBe(``) }) test('teleport rendering (vnode)', async () => { @@ -74,7 +76,9 @@ describe('ssrRenderTeleport', () => { ctx ) expect(html).toBe('') - expect(ctx.teleports!['#target']).toBe('hello') + expect(ctx.teleports!['#target']).toBe( + 'hello' + ) }) test('teleport rendering (vnode + disabled)', async () => { @@ -93,7 +97,7 @@ 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,7 @@ describe('ssrRenderTeleport', () => { '
' ) expect(ctx.teleports!['#target']).toBe( - 'helloworld' + 'helloworld' ) }) @@ -133,7 +137,9 @@ describe('ssrRenderTeleport', () => { ctx ) expect(html).toBe('') - expect(ctx.teleports!['#target']).toBe(`
content
`) + expect(ctx.teleports!['#target']).toBe( + `
content
` + ) }) test('teleport inside async component (stream)', async () => { @@ -166,6 +172,8 @@ describe('ssrRenderTeleport', () => { ) await p expect(html).toBe('') - expect(ctx.teleports!['#target']).toBe(`
content
`) + expect(ctx.teleports!['#target']).toBe( + `
content
` + ) }) }) diff --git a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts index 77331b7bddd..8338ec06c25 100644 --- a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts +++ b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts @@ -10,28 +10,28 @@ export function ssrRenderTeleport( ) { parentPush('') + const context = parentComponent.appContext.provides[ + ssrContextKey as any + ] as SSRContext + const teleportBuffers = + context.__teleportBuffers || (context.__teleportBuffers = {}) + const targetBuffer = teleportBuffers[target] || (teleportBuffers[target] = []) + // record current index of the target buffer to handle nested teleports + // since the parent needs to be rendered before the child + const bufferIndex = targetBuffer.length + let teleportContent: SSRBufferItem if (disabled) { contentRenderFn(parentPush) - teleportContent = `` + teleportContent = `` } else { const { getBuffer, push } = createBuffer() contentRenderFn(push) - push(``) // teleport end anchor + push(``) teleportContent = getBuffer() } - const context = parentComponent.appContext.provides[ - ssrContextKey as any - ] as SSRContext - const teleportBuffers = - context.__teleportBuffers || (context.__teleportBuffers = {}) - if (teleportBuffers[target]) { - teleportBuffers[target].push(teleportContent) - } else { - teleportBuffers[target] = [teleportContent] - } - + targetBuffer.splice(bufferIndex, 0, teleportContent) parentPush('') }