feat(ssr): hydration mismatch handling

This commit is contained in:
Evan You 2020-03-03 15:12:38 -06:00
parent 7971b0468c
commit 91269da52c

View File

@ -17,6 +17,14 @@ export type RootHydrateFunction = (
container: Element container: Element
) => void ) => void
const enum DOMNodeTypes {
ELEMENT = 1,
TEXT = 3,
COMMENT = 8
}
let hasHydrationMismatch = false
// Note: hydration is DOM-specific // Note: hydration is DOM-specific
// But we have to place it in core due to tight coupling with core - splitting // But we have to place it in core due to tight coupling with core - splitting
// it out creates a ton of unnecessary complexity. // it out creates a ton of unnecessary complexity.
@ -24,18 +32,27 @@ export type RootHydrateFunction = (
// passed in via arguments. // passed in via arguments.
export function createHydrationFunctions({ export function createHydrationFunctions({
mt: mountComponent, mt: mountComponent,
p: patch,
o: { patchProp, createText } o: { patchProp, createText }
}: RendererInternals<Node, Element>) { }: RendererInternals<Node, Element>) {
const hydrate: RootHydrateFunction = (vnode, container) => { const hydrate: RootHydrateFunction = (vnode, container) => {
if (__DEV__ && !container.hasChildNodes()) { 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 return
} }
hasHydrationMismatch = false
hydrateNode(container.firstChild!, vnode) hydrateNode(container.firstChild!, vnode)
flushPostFlushCbs() flushPostFlushCbs()
if (hasHydrationMismatch) {
// this error should show up in production
console.error(`Hydration completed but contains mismatches.`)
}
} }
// TODO handle mismatches
const hydrateNode = ( const hydrateNode = (
node: Node, node: Node,
vnode: VNode, vnode: VNode,
@ -43,16 +60,43 @@ export function createHydrationFunctions({
optimized = false optimized = false
): Node | null => { ): Node | null => {
const { type, shapeFlag } = vnode const { type, shapeFlag } = vnode
const domType = node.nodeType
vnode.el = node vnode.el = node
switch (type) { switch (type) {
case Text: 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: case Comment:
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent)
}
return node.nextSibling
case Static: case Static:
if (domType !== DOMNodeTypes.ELEMENT) {
return handleMismtach(node, vnode, parentComponent)
}
return node.nextSibling return node.nextSibling
case Fragment: case Fragment:
return hydrateFragment(node, vnode, parentComponent, optimized) return hydrateFragment(node, vnode, parentComponent, optimized)
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {
if (domType !== DOMNodeTypes.ELEMENT) {
return handleMismtach(node, vnode, parentComponent)
}
return hydrateElement( return hydrateElement(
node as Element, node as Element,
vnode, vnode,
@ -67,7 +111,15 @@ export function createHydrationFunctions({
const subTree = vnode.component!.subTree const subTree = vnode.component!.subTree
return (subTree.anchor || subTree.el).nextSibling return (subTree.anchor || subTree.el).nextSibling
} else if (shapeFlag & ShapeFlags.PORTAL) { } 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 return node.nextSibling
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// TODO Suspense // TODO Suspense
@ -84,7 +136,7 @@ export function createHydrationFunctions({
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
optimized: boolean optimized: boolean
) => { ) => {
const { props, patchFlag } = vnode const { props, patchFlag, shapeFlag } = vnode
// skip props & children if this is hoisted static nodes // skip props & children if this is hoisted static nodes
if (patchFlag !== PatchFlags.HOISTED) { if (patchFlag !== PatchFlags.HOISTED) {
// props // props
@ -116,16 +168,31 @@ export function createHydrationFunctions({
} }
// children // children
if ( if (
vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN && shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
// skip if element has innerHTML / textContent // skip if element has innerHTML / textContent
!(props !== null && (props.innerHTML || props.textContent)) !(props !== null && (props.innerHTML || props.textContent))
) { ) {
hydrateChildren( let next = hydrateChildren(
el.firstChild, el.firstChild,
vnode, vnode,
el,
parentComponent, parentComponent,
optimized || vnode.dynamicChildren !== null 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 return el.nextSibling
@ -134,16 +201,28 @@ export function createHydrationFunctions({
const hydrateChildren = ( const hydrateChildren = (
node: Node | null, node: Node | null,
vnode: VNode, vnode: VNode,
container: Element,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
optimized: boolean optimized: boolean
): Node | null => { ): Node | null => {
const children = vnode.children as VNode[] const children = vnode.children as VNode[]
optimized = optimized || vnode.dynamicChildren !== null 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 const vnode = optimized
? children[i] ? children[i]
: (children[i] = normalizeVNode(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 return node
} }
@ -154,15 +233,22 @@ export function createHydrationFunctions({
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
optimized: boolean optimized: boolean
) => { ) => {
const parent = node.parentNode! const parent = node.parentNode as Element
parent.insertBefore((vnode.el = createText('')), node) 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) parent.insertBefore((vnode.anchor = createText('')), next)
return next return next
} }
const hydratePortal = ( const hydratePortal = (
vnode: VNode, vnode: VNode,
container: Element,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
optimized: boolean optimized: boolean
) => { ) => {
@ -171,9 +257,37 @@ export function createHydrationFunctions({
? document.querySelector(targetSelector) ? document.querySelector(targetSelector)
: targetSelector) : targetSelector)
if (target != null && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 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 return [hydrate, hydrateNode] as const
} }