Explorar o código

fix(runtime-vapor): avoid preserving stale element mismatch content (#14834)

edison hai 4 semanas
pai
achega
e2af00015d

+ 65 - 0
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -6849,6 +6849,71 @@ describe('mismatch handling', () => {
     expect(container.innerHTML).toBe('<span>foo</span><!--dynamic-component-->')
     expect(`Hydration node mismatch`).toHaveBeenWarned()
   })
+  test('element mismatch should use client template static content', () => {
+    const container = document.createElement('div')
+    container.innerHTML =
+      '<span class="server-only">server text</span><i>after</i>'
+
+    setIsHydratingEnabled(true)
+    try {
+      hydrateNode(container.firstChild!, () => {
+        const n0 = template(
+          '<div class="client-only">client text</div>',
+        )() as HTMLElement
+
+        expect(n0).toBe(container.firstChild)
+      })
+    } finally {
+      setIsHydratingEnabled(false)
+    }
+
+    expect(container.innerHTML).toBe(
+      '<div class="client-only">client text</div><i>after</i>',
+    )
+    expect(`Hydration node mismatch`).toHaveBeenWarned()
+  })
+  test('dynamic component element mismatch should adopt slot children', async () => {
+    const data = ref('foo')
+    const { container } = await mountWithHydration(
+      '<span><b>foo</b></span>',
+      `<component :is="'div'"><b>{{ data }}</b></component>`,
+      data,
+    )
+
+    expect(container.innerHTML).toBe(
+      '<div><b>foo</b></div><!--dynamic-component-->',
+    )
+    expect(`Hydration node mismatch`).toHaveBeenWarned()
+
+    data.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toBe(
+      '<div><b>bar</b></div><!--dynamic-component-->',
+    )
+  })
+  test('dynamic component element mismatch should not adopt named slot children', async () => {
+    const data = ref({ name: 'foo', msg: 'client' })
+    const { container } = await mountWithHydration(
+      '<span><b>stale</b></span>',
+      `<component :is="'div'">
+        <template v-slot:[data.name]>
+          <b>{{ data.msg }}</b>
+        </template>
+      </component>`,
+      data,
+    )
+
+    expect(container.innerHTML).toBe(
+      '<div><!----></div><!--dynamic-component-->',
+    )
+    expect(`Hydration node mismatch`).toHaveBeenWarned()
+
+    data.value = { name: 'default', msg: 'updated' }
+    await nextTick()
+    expect(container.innerHTML).toBe(
+      '<div><b>updated</b><!----></div><!--dynamic-component-->',
+    )
+  })
   test('v-if empty branch should remove stale branch before trailing sibling', async () => {
     const code = `
       <div>

+ 10 - 1
packages/runtime-vapor/src/component.ts

@@ -945,8 +945,17 @@ export function createPlainElement(
     resetInsertionState()
   }
 
+  const defaultSlot = rawSlots && getSlot(rawSlots as RawSlots, 'default')
+  const hasDynamicSlots = !!rawSlots && !!rawSlots.$
+  const adoptHydrationChildren = !!defaultSlot
+  const hydrationTemplate =
+    hasDynamicSlots && !defaultSlot ? `<${comp}><!></${comp}>` : `<${comp}/>`
   const el = isHydrating
-    ? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
+    ? (adoptTemplate(
+        currentHydrationNode!,
+        hydrationTemplate,
+        adoptHydrationChildren,
+      ) as HTMLElement)
     : createElement(comp)
 
   // mark single root

+ 23 - 11
packages/runtime-vapor/src/dom/hydration.ts

@@ -126,7 +126,11 @@ export function enterHydration(node: Node): () => void {
   }
 }
 
-export let adoptTemplate: (node: Node, template: string) => Node | null
+export let adoptTemplate: (
+  node: Node,
+  template: string,
+  adoptChildren?: boolean,
+) => Node | null
 export let locateHydrationNode: (consumeFragmentStart?: boolean) => void
 
 type Anchor = Node & {
@@ -205,7 +209,11 @@ export function exitHydrationCursor(cursor: HydrationCursor | null): void {
  * Locate the first non-fragment-comment node and locate the next node
  * while handling potential fragments.
  */
-function adoptTemplateImpl(node: Node, template: string): Node | null {
+function adoptTemplateImpl(
+  node: Node,
+  template: string,
+  adoptChildren = false,
+): Node | null {
   if (!(template[0] === '<' && template[1] === '!')) {
     // empty text node in slot
     if (
@@ -227,7 +235,7 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
     (type === 1 &&
       !template.startsWith(`<` + (node as Element).tagName.toLowerCase()))
   ) {
-    node = handleMismatch(node, template)
+    node = handleMismatch(node, template, adoptChildren)
   }
 
   advanceHydrationNode(node)
@@ -322,7 +330,11 @@ export function locateHydrationBoundaryClose(
   return close
 }
 
-function handleMismatch(node: Node, template: string): Node {
+function handleMismatch(
+  node: Node,
+  template: string,
+  adoptChildren: boolean,
+): Node {
   warnHydrationNodeMismatch(node, template)
 
   // fragment
@@ -349,13 +361,13 @@ function handleMismatch(node: Node, template: string): Node {
   const t = createElement('template') as HTMLTemplateElement
   t.innerHTML = template
   const newNode = _child(t.content).cloneNode(true) as Element
-  // only carry over existing children/attrs when the original node is itself
-  // an element (the legacy element-vs-element mismatch case).
-  if (node.nodeType === 1) {
-    newNode.innerHTML = (node as Element).innerHTML
-    Array.from((node as Element).attributes).forEach(attr => {
-      newNode.setAttribute(attr.name, attr.value)
-    })
+  if (adoptChildren && node.nodeType === 1 && !newNode.firstChild) {
+    let child = node.firstChild
+    while (child) {
+      const nextChild = child.nextSibling
+      newNode.appendChild(child)
+      child = nextChild
+    }
   }
   container.insertBefore(newNode, next)
   return newNode