]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(ssr): improve fragment mismatch handling
authorEvan You <yyx990803@gmail.com>
Fri, 13 Mar 2020 22:02:53 +0000 (18:02 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 13 Mar 2020 22:02:53 +0000 (18:02 -0400)
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts

index 5d40b4e5d01c20c451100468f05d2474fd595fd6..67387bc52be9036e929d91649e1ed007c681a84e 100644 (file)
@@ -110,8 +110,9 @@ describe('SSR hydration', () => {
     )
     expect(vnode.el).toBe(container.firstChild)
 
-    // should remove anchors in dev mode
-    expect(vnode.el.innerHTML).toBe(`<span>foo</span><span class="foo"></span>`)
+    expect(vnode.el.innerHTML).toBe(
+      `<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`
+    )
 
     // start fragment 1
     const fragment1 = (vnode.children as VNode[])[0]
@@ -143,7 +144,9 @@ describe('SSR hydration', () => {
 
     msg.value = 'bar'
     await nextTick()
-    expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
+    expect(vnode.el.innerHTML).toBe(
+      `<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`
+    )
   })
 
   test('Portal', async () => {
@@ -363,7 +366,6 @@ describe('SSR hydration', () => {
 
     // should flush buffered effects
     expect(mountedCalls).toMatchObject([1, 2])
-    // should have removed fragment markers
     expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
 
     const span1 = container.querySelector('span')!
@@ -419,5 +421,40 @@ describe('SSR hydration', () => {
       expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
       expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
     })
+
+    test('fragment mismatch removal', () => {
+      const { container } = mountWithHydration(
+        `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
+        () => h('div', [h('span', 'replaced')])
+      )
+      expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
+      expect(`Hydration node mismatch`).toHaveBeenWarned()
+    })
+
+    test('fragment not enough children', () => {
+      const { container } = mountWithHydration(
+        `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
+        () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
+      )
+      expect(container.innerHTML).toBe(
+        '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
+      )
+      expect(`Hydration node mismatch`).toHaveBeenWarned()
+    })
+
+    test('fragment too many children', () => {
+      const { container } = mountWithHydration(
+        `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
+        () => h('div', [[h('div', 'foo')], h('div', 'baz')])
+      )
+      expect(container.innerHTML).toBe(
+        '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
+      )
+      // fragment ends early and attempts to hydrate the extra <div>bar</div>
+      // as 2nd fragment child.
+      expect(`Hydration text content mismatch`).toHaveBeenWarned()
+      // exccesive children removal
+      expect(`Hydration children mismatch`).toHaveBeenWarned()
+    })
   })
 })
index c0ee245edb1d381d186a2918fffee44bbc3453a5..945fb8a3075d56d8b3fb3fb781cec398eab5bac0 100644 (file)
@@ -47,7 +47,7 @@ export function createHydrationFunctions(
   const {
     mt: mountComponent,
     p: patch,
-    o: { patchProp, nextSibling, parentNode }
+    o: { patchProp, nextSibling, parentNode, remove, insert, createComment }
   } = rendererInternals
 
   const hydrate: RootHydrateFunction = (vnode, container) => {
@@ -76,11 +76,14 @@ export function createHydrationFunctions(
     optimized = false
   ): Node | null => {
     const isFragmentStart = isComment(node) && node.data === '['
-    if (__DEV__ && isFragmentStart) {
-      // in dev mode, replace comment anchors with invisible text nodes
-      // for easier debugging
-      node = replaceAnchor(node, parentNode(node)!)
-    }
+    const onMismatch = () =>
+      handleMismtach(
+        node,
+        vnode,
+        parentComponent,
+        parentSuspense,
+        isFragmentStart
+      )
 
     const { type, shapeFlag } = vnode
     const domType = node.nodeType
@@ -89,7 +92,7 @@ export function createHydrationFunctions(
     switch (type) {
       case Text:
         if (domType !== DOMNodeTypes.TEXT) {
-          return handleMismtach(node, vnode, parentComponent, parentSuspense)
+          return onMismatch()
         }
         if ((node as Text).data !== vnode.children) {
           hasMismatch = true
@@ -103,18 +106,18 @@ export function createHydrationFunctions(
         }
         return nextSibling(node)
       case Comment:
-        if (domType !== DOMNodeTypes.COMMENT) {
-          return handleMismtach(node, vnode, parentComponent, parentSuspense)
+        if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
+          return onMismatch()
         }
         return nextSibling(node)
       case Static:
         if (domType !== DOMNodeTypes.ELEMENT) {
-          return handleMismtach(node, vnode, parentComponent, parentSuspense)
+          return onMismatch()
         }
         return nextSibling(node)
       case Fragment:
-        if (domType !== (__DEV__ ? DOMNodeTypes.TEXT : DOMNodeTypes.COMMENT)) {
-          return handleMismtach(node, vnode, parentComponent, parentSuspense)
+        if (!isFragmentStart) {
+          return onMismatch()
         }
         return hydrateFragment(
           node as Comment,
@@ -129,7 +132,7 @@ export function createHydrationFunctions(
             domType !== DOMNodeTypes.ELEMENT ||
             vnode.type !== (node as Element).tagName.toLowerCase()
           ) {
-            return handleMismtach(node, vnode, parentComponent, parentSuspense)
+            return onMismatch()
           }
           return hydrateElement(
             node as Element,
@@ -159,7 +162,7 @@ export function createHydrationFunctions(
             : nextSibling(node)
         } else if (shapeFlag & ShapeFlags.PORTAL) {
           if (domType !== DOMNodeTypes.COMMENT) {
-            return handleMismtach(node, vnode, parentComponent, parentSuspense)
+            return onMismatch()
           }
           hydratePortal(vnode, parentComponent, parentSuspense, optimized)
           return nextSibling(node)
@@ -247,7 +250,7 @@ export function createHydrationFunctions(
           // The SSRed DOM contains more nodes than it should. Remove them.
           const cur = next
           next = next.nextSibling
-          el.removeChild(cur)
+          remove(cur)
         }
       } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
         if (el.textContent !== vnode.children) {
@@ -321,18 +324,24 @@ export function createHydrationFunctions(
     optimized: boolean
   ) => {
     const container = parentNode(node)!
-    let next = hydrateChildren(
+    const next = hydrateChildren(
       nextSibling(node)!,
       vnode,
       container,
       parentComponent,
       parentSuspense,
       optimized
-    )!
-    if (__DEV__) {
-      next = replaceAnchor(next, container)
+    )
+    if (next && isComment(next) && next.data === ']') {
+      return nextSibling((vnode.anchor = next))
+    } else {
+      // fragment didn't hydrate successfully, since we didn't get a end anchor
+      // back. This should have led to node/children mismatch warnings.
+      hasMismatch = true
+      // since the anchor is missing, we need to create one and insert it
+      insert((vnode.anchor = createComment(`]`)), container, next)
+      return next
     }
-    return nextSibling((vnode.anchor = next))
   }
 
   const hydratePortal = (
@@ -366,7 +375,8 @@ export function createHydrationFunctions(
     node: Node,
     vnode: VNode,
     parentComponent: ComponentInternalInstance | null,
-    parentSuspense: SuspenseBoundary | null
+    parentSuspense: SuspenseBoundary | null,
+    isFragment: boolean
   ) => {
     hasMismatch = true
     __DEV__ &&
@@ -375,12 +385,31 @@ export function createHydrationFunctions(
         vnode.type,
         `\n- Server rendered DOM:`,
         node,
-        node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
+        node.nodeType === DOMNodeTypes.TEXT
+          ? `(text)`
+          : isComment(node) && node.data === '['
+            ? `(start of fragment)`
+            : ``
       )
     vnode.el = null
+
+    if (isFragment) {
+      // remove excessive fragment nodes
+      const end = locateClosingAsyncAnchor(node)
+      while (true) {
+        const next = nextSibling(node)
+        if (next && next !== end) {
+          remove(next)
+        } else {
+          break
+        }
+      }
+    }
+
     const next = nextSibling(node)
     const container = parentNode(node)!
-    container.removeChild(node)
+    remove(node)
+
     patch(
       null,
       vnode,
@@ -411,12 +440,5 @@ export function createHydrationFunctions(
     return node
   }
 
-  const replaceAnchor = (node: Node, parent: Element): Node => {
-    const text = document.createTextNode('')
-    parent.insertBefore(text, node)
-    parent.removeChild(node)
-    return text
-  }
-
   return [hydrate, hydrateNode] as const
 }