diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 844b30a0ea9..061f5e05d96 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -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) { 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 }