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