Просмотр исходного кода

fix(runtime-vapor): preserve namespace during hydration recovery (#14837)

edison 4 недель назад
Родитель
Сommit
01e3fe0364

+ 42 - 1
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -23,7 +23,7 @@ import {
   ref,
   withDirectives,
 } from '@vue/runtime-dom'
-import { isString } from '@vue/shared'
+import { Namespaces, isString } from '@vue/shared'
 import type { VaporComponentInstance } from '../src/component'
 import type { TeleportFragment } from '../src/components/Teleport'
 import { VueServerRenderer, compile, runtimeDom, runtimeVapor } from './_utils'
@@ -6914,6 +6914,47 @@ describe('mismatch handling', () => {
       '<div><b>updated</b><!----></div><!--dynamic-component-->',
     )
   })
+
+  test('SVG child mismatch preserves namespace', () => {
+    setIsHydratingEnabled(true)
+    try {
+      const container = document.createElement('div')
+      container.innerHTML = `<svg><rect></rect></svg>`
+      const svg = container.firstChild as SVGElement
+
+      hydrateNode(svg.firstChild!, () => {
+        template('<circle></circle>', false, false, Namespaces.SVG)()
+      })
+      expect(`Hydration node mismatch`).toHaveBeenWarned()
+
+      const circle = svg.firstChild as SVGElement
+      expect(circle.localName).toBe('circle')
+      expect(circle.namespaceURI).toBe('http://www.w3.org/2000/svg')
+    } finally {
+      setIsHydratingEnabled(false)
+    }
+  })
+
+  test('MathML child mismatch preserves namespace', () => {
+    setIsHydratingEnabled(true)
+    try {
+      const container = document.createElement('div')
+      container.innerHTML = `<math><mn></mn></math>`
+      const math = container.firstChild as MathMLElement
+
+      hydrateNode(math.firstChild!, () => {
+        template('<mi></mi>', false, false, Namespaces.MATH_ML)()
+      })
+      expect(`Hydration node mismatch`).toHaveBeenWarned()
+
+      const mi = math.firstChild as MathMLElement
+      expect(mi.localName).toBe('mi')
+      expect(mi.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML')
+    } finally {
+      setIsHydratingEnabled(false)
+    }
+  })
+
   test('v-if empty branch should remove stale branch before trailing sibling', async () => {
     const code = `
       <div>

+ 14 - 3
packages/runtime-vapor/src/dom/hydration.ts

@@ -4,6 +4,7 @@ import {
   isHydrating as isVdomHydrating,
   warn,
 } from '@vue/runtime-dom'
+import { type Namespace, Namespaces } from '@vue/shared'
 import {
   insertionIndex,
   insertionParent,
@@ -130,6 +131,7 @@ export let adoptTemplate: (
   node: Node,
   template: string,
   adoptChildren?: boolean,
+  ns?: Namespace,
 ) => Node | null
 export let locateHydrationNode: (consumeFragmentStart?: boolean) => void
 
@@ -213,6 +215,7 @@ function adoptTemplateImpl(
   node: Node,
   template: string,
   adoptChildren = false,
+  ns?: Namespace,
 ): Node | null {
   if (!(template[0] === '<' && template[1] === '!')) {
     // empty text node in slot
@@ -235,7 +238,7 @@ function adoptTemplateImpl(
     (type === 1 &&
       !template.startsWith(`<` + (node as Element).tagName.toLowerCase()))
   ) {
-    node = handleMismatch(node, template, adoptChildren)
+    node = handleMismatch(node, template, adoptChildren, ns)
   }
 
   advanceHydrationNode(node)
@@ -334,6 +337,7 @@ function handleMismatch(
   node: Node,
   template: string,
   adoptChildren: boolean,
+  ns?: Namespace,
 ): Node {
   warnHydrationNodeMismatch(node, template)
 
@@ -359,8 +363,15 @@ function handleMismatch(
 
   // element node
   const t = createElement('template') as HTMLTemplateElement
-  t.innerHTML = template
-  const newNode = _child(t.content).cloneNode(true) as Element
+  let newNode: Element
+  if (ns) {
+    const tag = ns === Namespaces.SVG ? 'svg' : 'math'
+    t.innerHTML = `<${tag}>${template}</${tag}>`
+    newNode = _child(_child(t.content) as ParentNode).cloneNode(true) as Element
+  } else {
+    t.innerHTML = template
+    newNode = _child(t.content).cloneNode(true) as Element
+  }
   if (adoptChildren && node.nodeType === 1 && !newNode.firstChild) {
     let child = node.firstChild
     while (child) {

+ 1 - 1
packages/runtime-vapor/src/dom/template.ts

@@ -39,7 +39,7 @@ export function template(
       } else {
         // do not cache the adopted node in node because it contains child nodes
         // this avoids duplicate rendering of children
-        adopted = adoptTemplate(currentHydrationNode!, html)!
+        adopted = adoptTemplate(currentHydrationNode!, html, false, ns)!
       }
       if (root) (adopted as any).$root = true
       return adopted