feat(ssr): improve fragment mismatch handling
This commit is contained in:
parent
eb1d538ea2
commit
60ed4e7e08
@ -110,8 +110,9 @@ describe('SSR hydration', () => {
|
|||||||
)
|
)
|
||||||
expect(vnode.el).toBe(container.firstChild)
|
expect(vnode.el).toBe(container.firstChild)
|
||||||
|
|
||||||
// should remove anchors in dev mode
|
expect(vnode.el.innerHTML).toBe(
|
||||||
expect(vnode.el.innerHTML).toBe(`<span>foo</span><span class="foo"></span>`)
|
`<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`
|
||||||
|
)
|
||||||
|
|
||||||
// start fragment 1
|
// start fragment 1
|
||||||
const fragment1 = (vnode.children as VNode[])[0]
|
const fragment1 = (vnode.children as VNode[])[0]
|
||||||
@ -143,7 +144,9 @@ describe('SSR hydration', () => {
|
|||||||
|
|
||||||
msg.value = 'bar'
|
msg.value = 'bar'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
|
expect(vnode.el.innerHTML).toBe(
|
||||||
|
`<!--[--><span>bar</span><!--[--><span class="bar"></span><!--]--><!--]-->`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Portal', async () => {
|
test('Portal', async () => {
|
||||||
@ -363,7 +366,6 @@ describe('SSR hydration', () => {
|
|||||||
|
|
||||||
// should flush buffered effects
|
// should flush buffered effects
|
||||||
expect(mountedCalls).toMatchObject([1, 2])
|
expect(mountedCalls).toMatchObject([1, 2])
|
||||||
// should have removed fragment markers
|
|
||||||
expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
|
expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
|
||||||
|
|
||||||
const span1 = container.querySelector('span')!
|
const span1 = container.querySelector('span')!
|
||||||
@ -419,5 +421,40 @@ describe('SSR hydration', () => {
|
|||||||
expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
|
expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
|
||||||
expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
|
expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('fragment mismatch removal', () => {
|
||||||
|
const { container } = mountWithHydration(
|
||||||
|
`<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
|
||||||
|
() => h('div', [h('span', 'replaced')])
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
|
||||||
|
expect(`Hydration node mismatch`).toHaveBeenWarned()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fragment not enough children', () => {
|
||||||
|
const { container } = mountWithHydration(
|
||||||
|
`<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
|
||||||
|
() => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')])
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>'
|
||||||
|
)
|
||||||
|
expect(`Hydration node mismatch`).toHaveBeenWarned()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('fragment too many children', () => {
|
||||||
|
const { container } = mountWithHydration(
|
||||||
|
`<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
|
||||||
|
() => h('div', [[h('div', 'foo')], h('div', 'baz')])
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div><!--[--><div>foo</div><!--]--><div>baz</div></div>'
|
||||||
|
)
|
||||||
|
// fragment ends early and attempts to hydrate the extra <div>bar</div>
|
||||||
|
// as 2nd fragment child.
|
||||||
|
expect(`Hydration text content mismatch`).toHaveBeenWarned()
|
||||||
|
// exccesive children removal
|
||||||
|
expect(`Hydration children mismatch`).toHaveBeenWarned()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -47,7 +47,7 @@ export function createHydrationFunctions(
|
|||||||
const {
|
const {
|
||||||
mt: mountComponent,
|
mt: mountComponent,
|
||||||
p: patch,
|
p: patch,
|
||||||
o: { patchProp, nextSibling, parentNode }
|
o: { patchProp, nextSibling, parentNode, remove, insert, createComment }
|
||||||
} = rendererInternals
|
} = rendererInternals
|
||||||
|
|
||||||
const hydrate: RootHydrateFunction = (vnode, container) => {
|
const hydrate: RootHydrateFunction = (vnode, container) => {
|
||||||
@ -76,11 +76,14 @@ export function createHydrationFunctions(
|
|||||||
optimized = false
|
optimized = false
|
||||||
): Node | null => {
|
): Node | null => {
|
||||||
const isFragmentStart = isComment(node) && node.data === '['
|
const isFragmentStart = isComment(node) && node.data === '['
|
||||||
if (__DEV__ && isFragmentStart) {
|
const onMismatch = () =>
|
||||||
// in dev mode, replace comment anchors with invisible text nodes
|
handleMismtach(
|
||||||
// for easier debugging
|
node,
|
||||||
node = replaceAnchor(node, parentNode(node)!)
|
vnode,
|
||||||
}
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
isFragmentStart
|
||||||
|
)
|
||||||
|
|
||||||
const { type, shapeFlag } = vnode
|
const { type, shapeFlag } = vnode
|
||||||
const domType = node.nodeType
|
const domType = node.nodeType
|
||||||
@ -89,7 +92,7 @@ export function createHydrationFunctions(
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case Text:
|
case Text:
|
||||||
if (domType !== DOMNodeTypes.TEXT) {
|
if (domType !== DOMNodeTypes.TEXT) {
|
||||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
return onMismatch()
|
||||||
}
|
}
|
||||||
if ((node as Text).data !== vnode.children) {
|
if ((node as Text).data !== vnode.children) {
|
||||||
hasMismatch = true
|
hasMismatch = true
|
||||||
@ -103,18 +106,18 @@ export function createHydrationFunctions(
|
|||||||
}
|
}
|
||||||
return nextSibling(node)
|
return nextSibling(node)
|
||||||
case Comment:
|
case Comment:
|
||||||
if (domType !== DOMNodeTypes.COMMENT) {
|
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
|
||||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
return onMismatch()
|
||||||
}
|
}
|
||||||
return nextSibling(node)
|
return nextSibling(node)
|
||||||
case Static:
|
case Static:
|
||||||
if (domType !== DOMNodeTypes.ELEMENT) {
|
if (domType !== DOMNodeTypes.ELEMENT) {
|
||||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
return onMismatch()
|
||||||
}
|
}
|
||||||
return nextSibling(node)
|
return nextSibling(node)
|
||||||
case Fragment:
|
case Fragment:
|
||||||
if (domType !== (__DEV__ ? DOMNodeTypes.TEXT : DOMNodeTypes.COMMENT)) {
|
if (!isFragmentStart) {
|
||||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
return onMismatch()
|
||||||
}
|
}
|
||||||
return hydrateFragment(
|
return hydrateFragment(
|
||||||
node as Comment,
|
node as Comment,
|
||||||
@ -129,7 +132,7 @@ export function createHydrationFunctions(
|
|||||||
domType !== DOMNodeTypes.ELEMENT ||
|
domType !== DOMNodeTypes.ELEMENT ||
|
||||||
vnode.type !== (node as Element).tagName.toLowerCase()
|
vnode.type !== (node as Element).tagName.toLowerCase()
|
||||||
) {
|
) {
|
||||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
return onMismatch()
|
||||||
}
|
}
|
||||||
return hydrateElement(
|
return hydrateElement(
|
||||||
node as Element,
|
node as Element,
|
||||||
@ -159,7 +162,7 @@ export function createHydrationFunctions(
|
|||||||
: nextSibling(node)
|
: nextSibling(node)
|
||||||
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
||||||
if (domType !== DOMNodeTypes.COMMENT) {
|
if (domType !== DOMNodeTypes.COMMENT) {
|
||||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
return onMismatch()
|
||||||
}
|
}
|
||||||
hydratePortal(vnode, parentComponent, parentSuspense, optimized)
|
hydratePortal(vnode, parentComponent, parentSuspense, optimized)
|
||||||
return nextSibling(node)
|
return nextSibling(node)
|
||||||
@ -247,7 +250,7 @@ export function createHydrationFunctions(
|
|||||||
// The SSRed DOM contains more nodes than it should. Remove them.
|
// The SSRed DOM contains more nodes than it should. Remove them.
|
||||||
const cur = next
|
const cur = next
|
||||||
next = next.nextSibling
|
next = next.nextSibling
|
||||||
el.removeChild(cur)
|
remove(cur)
|
||||||
}
|
}
|
||||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||||
if (el.textContent !== vnode.children) {
|
if (el.textContent !== vnode.children) {
|
||||||
@ -321,18 +324,24 @@ export function createHydrationFunctions(
|
|||||||
optimized: boolean
|
optimized: boolean
|
||||||
) => {
|
) => {
|
||||||
const container = parentNode(node)!
|
const container = parentNode(node)!
|
||||||
let next = hydrateChildren(
|
const next = hydrateChildren(
|
||||||
nextSibling(node)!,
|
nextSibling(node)!,
|
||||||
vnode,
|
vnode,
|
||||||
container,
|
container,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
parentSuspense,
|
parentSuspense,
|
||||||
optimized
|
optimized
|
||||||
)!
|
)
|
||||||
if (__DEV__) {
|
if (next && isComment(next) && next.data === ']') {
|
||||||
next = replaceAnchor(next, container)
|
|
||||||
}
|
|
||||||
return nextSibling((vnode.anchor = next))
|
return nextSibling((vnode.anchor = next))
|
||||||
|
} else {
|
||||||
|
// fragment didn't hydrate successfully, since we didn't get a end anchor
|
||||||
|
// back. This should have led to node/children mismatch warnings.
|
||||||
|
hasMismatch = true
|
||||||
|
// since the anchor is missing, we need to create one and insert it
|
||||||
|
insert((vnode.anchor = createComment(`]`)), container, next)
|
||||||
|
return next
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydratePortal = (
|
const hydratePortal = (
|
||||||
@ -366,7 +375,8 @@ export function createHydrationFunctions(
|
|||||||
node: Node,
|
node: Node,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
parentSuspense: SuspenseBoundary | null
|
parentSuspense: SuspenseBoundary | null,
|
||||||
|
isFragment: boolean
|
||||||
) => {
|
) => {
|
||||||
hasMismatch = true
|
hasMismatch = true
|
||||||
__DEV__ &&
|
__DEV__ &&
|
||||||
@ -375,12 +385,31 @@ export function createHydrationFunctions(
|
|||||||
vnode.type,
|
vnode.type,
|
||||||
`\n- Server rendered DOM:`,
|
`\n- Server rendered DOM:`,
|
||||||
node,
|
node,
|
||||||
node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
|
node.nodeType === DOMNodeTypes.TEXT
|
||||||
|
? `(text)`
|
||||||
|
: isComment(node) && node.data === '['
|
||||||
|
? `(start of fragment)`
|
||||||
|
: ``
|
||||||
)
|
)
|
||||||
vnode.el = null
|
vnode.el = null
|
||||||
|
|
||||||
|
if (isFragment) {
|
||||||
|
// remove excessive fragment nodes
|
||||||
|
const end = locateClosingAsyncAnchor(node)
|
||||||
|
while (true) {
|
||||||
|
const next = nextSibling(node)
|
||||||
|
if (next && next !== end) {
|
||||||
|
remove(next)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const next = nextSibling(node)
|
const next = nextSibling(node)
|
||||||
const container = parentNode(node)!
|
const container = parentNode(node)!
|
||||||
container.removeChild(node)
|
remove(node)
|
||||||
|
|
||||||
patch(
|
patch(
|
||||||
null,
|
null,
|
||||||
vnode,
|
vnode,
|
||||||
@ -411,12 +440,5 @@ export function createHydrationFunctions(
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceAnchor = (node: Node, parent: Element): Node => {
|
|
||||||
const text = document.createTextNode('')
|
|
||||||
parent.insertBefore(text, node)
|
|
||||||
parent.removeChild(node)
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
return [hydrate, hydrateNode] as const
|
return [hydrate, hydrateNode] as const
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user