]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(teleport/ssr): fix Teleport hydration regression due to targetStart anchor addition
authorEvan You <evan@vuejs.org>
Wed, 31 Jul 2024 08:09:56 +0000 (16:09 +0800)
committerEvan You <evan@vuejs.org>
Wed, 31 Jul 2024 08:10:23 +0000 (16:10 +0800)
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/components/Teleport.ts
packages/server-renderer/__tests__/ssrTeleport.spec.ts
packages/server-renderer/src/helpers/ssrRenderTeleport.ts

index 9916fafa62c9bcedade78dea6ff76a1bc22c1b26..c525618b510de7c6358b5cba16232ae6b7181279 100644 (file)
@@ -265,7 +265,7 @@ describe('SSR hydration', () => {
     const fn = vi.fn()
     const teleportContainer = document.createElement('div')
     teleportContainer.id = 'teleport'
-    teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
+    teleportContainer.innerHTML = `<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor-->`
     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(
-      `<span>bar</span><span class="bar"></span><!--teleport anchor-->`,
+      `<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor-->`,
     )
   })
 
@@ -326,7 +327,7 @@ describe('SSR hydration', () => {
 
     const teleportHtml = ctx.teleports!['#teleport2']
     expect(teleportHtml).toMatchInlineSnapshot(
-      `"<span>foo</span><span class="foo"></span><!--teleport anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
+      `"<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor--><!--teleport start anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
     )
 
     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(
-      `"<span>bar</span><span class="bar"></span><!--teleport anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
+      `"<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor--><!--teleport start anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
     )
   })
 
@@ -390,7 +393,9 @@ describe('SSR hydration', () => {
     )
 
     const teleportHtml = ctx.teleports!['#teleport3']
-    expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
+    expect(teleportHtml).toMatchInlineSnapshot(
+      `"<!--teleport start anchor--><!--teleport anchor-->"`,
+    )
 
     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<!--teleport anchor-->`
+    teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
     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 = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
+    teleportContainer.innerHTML = `<!--teleport start anchor--><div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><!--teleport start anchor--><div>child</div><!--teleport anchor-->`
     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(
+      '<div><!--teleport start--><!--teleport end--></div>',
+    )
+    teleportContainer.innerHTML = ctx.teleports!['#target']
+
+    // hydrate
+    createSSRApp(App).mount(container)
+    expect(container.innerHTML).toBe(
+      '<div><!--teleport start--><!--teleport end--></div>',
+    )
+    expect(teleportContainer.innerHTML).toBe(
+      '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
+    )
+    expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+
+    toggle.value = false
+    await nextTick()
+    expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+    expect(teleportContainer.innerHTML).toBe('')
+  })
+
+  test('Teleport unmount (mismatch + full integration)', async () => {
+    const Comp1 = {
+      template: `
+        <Teleport to="#target"> 
+          <span>Teleported Comp1</span>
+        </Teleport>
+      `,
+    }
+    const Comp2 = {
+      template: `
+        <div>Comp2</div>
+      `,
+    }
+
+    const toggle = ref(true)
+    const App = {
+      template: `
+        <div>
+          <Comp1 v-if="toggle"/>
+          <Comp2 v-else/>
+        </div>
+      `,
+      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: `
index 81573cc85a74823885059c91474752153162a7f7..d868fbbc6691dacf8f27ff58a226cf30d0efe158 100644 (file)
@@ -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,
index a0a2f6ae0a103ee56d554225d68521ddbed31ea7..78c56942636d373da7d505daf511ad3d6e8fbf7d 100644 (file)
@@ -28,7 +28,7 @@ describe('ssrRenderTeleport', () => {
     )
     expect(html).toBe('<!--teleport start--><!--teleport end-->')
     expect(ctx.teleports!['#target']).toBe(
-      `<div>content</div><!--teleport anchor-->`,
+      `<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
     )
   })
 
@@ -56,7 +56,9 @@ describe('ssrRenderTeleport', () => {
     expect(html).toBe(
       '<!--teleport start--><div>content</div><!--teleport end-->',
     )
-    expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
+    expect(ctx.teleports!['#target']).toBe(
+      `<!--teleport start anchor--><!--teleport anchor-->`,
+    )
   })
 
   test('teleport rendering (vnode)', async () => {
@@ -73,7 +75,7 @@ describe('ssrRenderTeleport', () => {
     )
     expect(html).toBe('<!--teleport start--><!--teleport end-->')
     expect(ctx.teleports!['#target']).toBe(
-      '<span>hello</span><!--teleport anchor-->',
+      '<!--teleport start anchor--><span>hello</span><!--teleport anchor-->',
     )
   })
 
@@ -93,7 +95,9 @@ describe('ssrRenderTeleport', () => {
     expect(html).toBe(
       '<!--teleport start--><span>hello</span><!--teleport end-->',
     )
-    expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
+    expect(ctx.teleports!['#target']).toBe(
+      `<!--teleport start anchor--><!--teleport anchor-->`,
+    )
   })
 
   test('multiple teleports with same target', async () => {
@@ -115,7 +119,8 @@ describe('ssrRenderTeleport', () => {
       '<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>',
     )
     expect(ctx.teleports!['#target']).toBe(
-      '<span>hello</span><!--teleport anchor-->world<!--teleport anchor-->',
+      '<!--teleport start anchor--><span>hello</span><!--teleport anchor-->' +
+        '<!--teleport start anchor-->world<!--teleport anchor-->',
     )
   })
 
@@ -134,7 +139,7 @@ describe('ssrRenderTeleport', () => {
     )
     expect(html).toBe('<!--teleport start--><!--teleport end-->')
     expect(ctx.teleports!['#target']).toBe(
-      `<div>content</div><!--teleport anchor-->`,
+      `<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
     )
   })
 
@@ -169,7 +174,7 @@ describe('ssrRenderTeleport', () => {
     await p
     expect(html).toBe('<!--teleport start--><!--teleport end-->')
     expect(ctx.teleports!['#target']).toBe(
-      `<div>content</div><!--teleport anchor-->`,
+      `<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
     )
   })
 })
index d83af28c131c08c527db873fdff7512c47c972a8..0806a3927bed7457f22ab73bba446e752795edf0 100644 (file)
@@ -29,9 +29,10 @@ export function ssrRenderTeleport(
 
   if (disabled) {
     contentRenderFn(parentPush)
-    teleportContent = `<!--teleport anchor-->`
+    teleportContent = `<!--teleport start anchor--><!--teleport anchor-->`
   } else {
     const { getBuffer, push } = createBuffer()
+    push(`<!--teleport start anchor-->`)
     contentRenderFn(push)
     push(`<!--teleport anchor-->`)
     teleportContent = getBuffer()