|
|
@@ -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
|
|
|
}
|