feat(ssr): hydration mismatch handling
This commit is contained in:
parent
7971b0468c
commit
91269da52c
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user