]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(ssr): hydration mismatch handling
authorEvan You <yyx990803@gmail.com>
Tue, 3 Mar 2020 21:12:38 +0000 (15:12 -0600)
committerEvan You <yyx990803@gmail.com>
Tue, 3 Mar 2020 21:12:38 +0000 (15:12 -0600)
packages/runtime-core/src/hydration.ts

index 844b30a0ea966129b8ac7160f15092f19b63a2a0..061f5e05d966b592daa47fc88d5e64dc843008eb 100644 (file)
@@ -17,6 +17,14 @@ export type RootHydrateFunction = (
   container: Element
 ) => void
 
+const enum DOMNodeTypes {
+  ELEMENT = 1,
+  TEXT = 3,
+  COMMENT = 8
+}
+
+let hasHydrationMismatch = false
+
 // Note: hydration is DOM-specific
 // But we have to place it in core due to tight coupling with core - splitting
 // it out creates a ton of unnecessary complexity.
@@ -24,18 +32,27 @@ export type RootHydrateFunction = (
 // passed in via arguments.
 export function createHydrationFunctions({
   mt: mountComponent,
+  p: patch,
   o: { patchProp, createText }
 }: RendererInternals<Node, Element>) {
   const hydrate: RootHydrateFunction = (vnode, container) => {
     if (__DEV__ && !container.hasChildNodes()) {
-      warn(`Attempting to hydrate existing markup but container is empty.`)
+      warn(
+        `Attempting to hydrate existing markup but container is empty. ` +
+          `Performing full mount instead.`
+      )
+      patch(null, vnode, container)
       return
     }
+    hasHydrationMismatch = false
     hydrateNode(container.firstChild!, vnode)
     flushPostFlushCbs()
+    if (hasHydrationMismatch) {
+      // this error should show up in production
+      console.error(`Hydration completed but contains mismatches.`)
+    }
   }
 
-  // TODO handle mismatches
   const hydrateNode = (
     node: Node,
     vnode: VNode,
@@ -43,16 +60,43 @@ export function createHydrationFunctions({
     optimized = false
   ): Node | null => {
     const { type, shapeFlag } = vnode
+    const domType = node.nodeType
+
     vnode.el = node
+
     switch (type) {
       case Text:
+        if (domType !== DOMNodeTypes.TEXT) {
+          return handleMismtach(node, vnode, parentComponent)
+        }
+        if ((node as Text).data !== vnode.children) {
+          hasHydrationMismatch = true
+          __DEV__ &&
+            warn(
+              `Hydration text mismatch:` +
+                `\n- Client: ${JSON.stringify(vnode.children)}`,
+              `\n- Server: ${JSON.stringify((node as Text).data)}`
+            )
+          ;(node as Text).data = vnode.children as string
+        }
+        return node.nextSibling
       case Comment:
+        if (domType !== DOMNodeTypes.COMMENT) {
+          return handleMismtach(node, vnode, parentComponent)
+        }
+        return node.nextSibling
       case Static:
+        if (domType !== DOMNodeTypes.ELEMENT) {
+          return handleMismtach(node, vnode, parentComponent)
+        }
         return node.nextSibling
       case Fragment:
         return hydrateFragment(node, vnode, parentComponent, optimized)
       default:
         if (shapeFlag & ShapeFlags.ELEMENT) {
+          if (domType !== DOMNodeTypes.ELEMENT) {
+            return handleMismtach(node, vnode, parentComponent)
+          }
           return hydrateElement(
             node as Element,
             vnode,
@@ -67,7 +111,15 @@ export function createHydrationFunctions({
           const subTree = vnode.component!.subTree
           return (subTree.anchor || subTree.el).nextSibling
         } else if (shapeFlag & ShapeFlags.PORTAL) {
-          hydratePortal(vnode, parentComponent, optimized)
+          if (domType !== DOMNodeTypes.COMMENT) {
+            return handleMismtach(node, vnode, parentComponent)
+          }
+          hydratePortal(
+            vnode,
+            node.parentNode as Element,
+            parentComponent,
+            optimized
+          )
           return node.nextSibling
         } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
           // TODO Suspense
@@ -84,7 +136,7 @@ export function createHydrationFunctions({
     parentComponent: ComponentInternalInstance | null,
     optimized: boolean
   ) => {
-    const { props, patchFlag } = vnode
+    const { props, patchFlag, shapeFlag } = vnode
     // skip props & children if this is hoisted static nodes
     if (patchFlag !== PatchFlags.HOISTED) {
       // props
@@ -116,16 +168,31 @@ export function createHydrationFunctions({
       }
       // children
       if (
-        vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
+        shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
         // skip if element has innerHTML / textContent
         !(props !== null && (props.innerHTML || props.textContent))
       ) {
-        hydrateChildren(
+        let next = hydrateChildren(
           el.firstChild,
           vnode,
+          el,
           parentComponent,
           optimized || vnode.dynamicChildren !== null
         )
+        while (next) {
+          hasHydrationMismatch = true
+          __DEV__ &&
+            warn(
+              `Hydration children mismatch: ` +
+                `server rendered element contains more child nodes than client vdom.`
+            )
+          // The SSRed DOM contains more nodes than it should. Remove them.
+          const cur = next
+          next = next.nextSibling
+          el.removeChild(cur)
+        }
+      } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
+        el.textContent = vnode.children as string
       }
     }
     return el.nextSibling
@@ -134,16 +201,28 @@ export function createHydrationFunctions({
   const hydrateChildren = (
     node: Node | null,
     vnode: VNode,
+    container: Element,
     parentComponent: ComponentInternalInstance | null,
     optimized: boolean
   ): Node | null => {
     const children = vnode.children as VNode[]
     optimized = optimized || vnode.dynamicChildren !== null
-    for (let i = 0; node != null && i < children.length; i++) {
+    for (let i = 0; i < children.length; i++) {
       const vnode = optimized
         ? children[i]
         : (children[i] = normalizeVNode(children[i]))
-      node = hydrateNode(node, vnode, parentComponent, optimized)
+      if (node) {
+        node = hydrateNode(node, vnode, parentComponent, optimized)
+      } else {
+        hasHydrationMismatch = true
+        __DEV__ &&
+          warn(
+            `Hydration children mismatch: ` +
+              `server rendered element contains fewer child nodes than client vdom.`
+          )
+        // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
+        patch(null, vnode, container)
+      }
     }
     return node
   }
@@ -154,15 +233,22 @@ export function createHydrationFunctions({
     parentComponent: ComponentInternalInstance | null,
     optimized: boolean
   ) => {
-    const parent = node.parentNode!
+    const parent = node.parentNode as Element
     parent.insertBefore((vnode.el = createText('')), node)
-    const next = hydrateChildren(node, vnode, parentComponent, optimized)
+    const next = hydrateChildren(
+      node,
+      vnode,
+      parent,
+      parentComponent,
+      optimized
+    )
     parent.insertBefore((vnode.anchor = createText('')), next)
     return next
   }
 
   const hydratePortal = (
     vnode: VNode,
+    container: Element,
     parentComponent: ComponentInternalInstance | null,
     optimized: boolean
   ) => {
@@ -171,9 +257,37 @@ export function createHydrationFunctions({
       ? document.querySelector(targetSelector)
       : targetSelector)
     if (target != null && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
-      hydrateChildren(target.firstChild, vnode, parentComponent, optimized)
+      hydrateChildren(
+        target.firstChild,
+        vnode,
+        container,
+        parentComponent,
+        optimized
+      )
     }
   }
 
+  const handleMismtach = (
+    node: Node,
+    vnode: VNode,
+    parentComponent: ComponentInternalInstance | null
+  ) => {
+    hasHydrationMismatch = true
+    __DEV__ &&
+      warn(
+        `Hydration node mismatch:\n- Client vnode:`,
+        vnode.type,
+        `\n- Server rendered DOM:`,
+        node
+      )
+    vnode.el = null
+    const next = node.nextSibling
+    const container = node.parentNode as Element
+    container.removeChild(node)
+    // TODO Suspense and SVG
+    patch(null, vnode, container, next, parentComponent)
+    return next
+  }
+
   return [hydrate, hydrateNode] as const
 }