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)
|
||||
|
||||
// should remove anchors in dev mode
|
||||
expect(vnode.el.innerHTML).toBe(`<span>foo</span><span class="foo"></span>`)
|
||||
expect(vnode.el.innerHTML).toBe(
|
||||
`<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`
|
||||
)
|
||||
|
||||
// start fragment 1
|
||||
const fragment1 = (vnode.children as VNode[])[0]
|
||||
@ -143,7 +144,9 @@ describe('SSR hydration', () => {
|
||||
|
||||
msg.value = 'bar'
|
||||
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 () => {
|
||||
@ -363,7 +366,6 @@ describe('SSR hydration', () => {
|
||||
|
||||
// should flush buffered effects
|
||||
expect(mountedCalls).toMatchObject([1, 2])
|
||||
// should have removed fragment markers
|
||||
expect(container.innerHTML).toMatch(`<span>1</span><span>2</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(`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 {
|
||||
mt: mountComponent,
|
||||
p: patch,
|
||||
o: { patchProp, nextSibling, parentNode }
|
||||
o: { patchProp, nextSibling, parentNode, remove, insert, createComment }
|
||||
} = rendererInternals
|
||||
|
||||
const hydrate: RootHydrateFunction = (vnode, container) => {
|
||||
@ -76,11 +76,14 @@ export function createHydrationFunctions(
|
||||
optimized = false
|
||||
): Node | null => {
|
||||
const isFragmentStart = isComment(node) && node.data === '['
|
||||
if (__DEV__ && isFragmentStart) {
|
||||
// in dev mode, replace comment anchors with invisible text nodes
|
||||
// for easier debugging
|
||||
node = replaceAnchor(node, parentNode(node)!)
|
||||
}
|
||||
const onMismatch = () =>
|
||||
handleMismtach(
|
||||
node,
|
||||
vnode,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
isFragmentStart
|
||||
)
|
||||
|
||||
const { type, shapeFlag } = vnode
|
||||
const domType = node.nodeType
|
||||
@ -89,7 +92,7 @@ export function createHydrationFunctions(
|
||||
switch (type) {
|
||||
case Text:
|
||||
if (domType !== DOMNodeTypes.TEXT) {
|
||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||
return onMismatch()
|
||||
}
|
||||
if ((node as Text).data !== vnode.children) {
|
||||
hasMismatch = true
|
||||
@ -103,18 +106,18 @@ export function createHydrationFunctions(
|
||||
}
|
||||
return nextSibling(node)
|
||||
case Comment:
|
||||
if (domType !== DOMNodeTypes.COMMENT) {
|
||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
|
||||
return onMismatch()
|
||||
}
|
||||
return nextSibling(node)
|
||||
case Static:
|
||||
if (domType !== DOMNodeTypes.ELEMENT) {
|
||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||
return onMismatch()
|
||||
}
|
||||
return nextSibling(node)
|
||||
case Fragment:
|
||||
if (domType !== (__DEV__ ? DOMNodeTypes.TEXT : DOMNodeTypes.COMMENT)) {
|
||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||
if (!isFragmentStart) {
|
||||
return onMismatch()
|
||||
}
|
||||
return hydrateFragment(
|
||||
node as Comment,
|
||||
@ -129,7 +132,7 @@ export function createHydrationFunctions(
|
||||
domType !== DOMNodeTypes.ELEMENT ||
|
||||
vnode.type !== (node as Element).tagName.toLowerCase()
|
||||
) {
|
||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||
return onMismatch()
|
||||
}
|
||||
return hydrateElement(
|
||||
node as Element,
|
||||
@ -159,7 +162,7 @@ export function createHydrationFunctions(
|
||||
: nextSibling(node)
|
||||
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
||||
if (domType !== DOMNodeTypes.COMMENT) {
|
||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||
return onMismatch()
|
||||
}
|
||||
hydratePortal(vnode, parentComponent, parentSuspense, optimized)
|
||||
return nextSibling(node)
|
||||
@ -247,7 +250,7 @@ export function createHydrationFunctions(
|
||||
// The SSRed DOM contains more nodes than it should. Remove them.
|
||||
const cur = next
|
||||
next = next.nextSibling
|
||||
el.removeChild(cur)
|
||||
remove(cur)
|
||||
}
|
||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
if (el.textContent !== vnode.children) {
|
||||
@ -321,18 +324,24 @@ export function createHydrationFunctions(
|
||||
optimized: boolean
|
||||
) => {
|
||||
const container = parentNode(node)!
|
||||
let next = hydrateChildren(
|
||||
const next = hydrateChildren(
|
||||
nextSibling(node)!,
|
||||
vnode,
|
||||
container,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
optimized
|
||||
)!
|
||||
if (__DEV__) {
|
||||
next = replaceAnchor(next, container)
|
||||
)
|
||||
if (next && isComment(next) && next.data === ']') {
|
||||
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
|
||||
}
|
||||
return nextSibling((vnode.anchor = next))
|
||||
}
|
||||
|
||||
const hydratePortal = (
|
||||
@ -366,7 +375,8 @@ export function createHydrationFunctions(
|
||||
node: Node,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
parentSuspense: SuspenseBoundary | null
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
isFragment: boolean
|
||||
) => {
|
||||
hasMismatch = true
|
||||
__DEV__ &&
|
||||
@ -375,12 +385,31 @@ export function createHydrationFunctions(
|
||||
vnode.type,
|
||||
`\n- Server rendered DOM:`,
|
||||
node,
|
||||
node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
|
||||
node.nodeType === DOMNodeTypes.TEXT
|
||||
? `(text)`
|
||||
: isComment(node) && node.data === '['
|
||||
? `(start of fragment)`
|
||||
: ``
|
||||
)
|
||||
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 container = parentNode(node)!
|
||||
container.removeChild(node)
|
||||
remove(node)
|
||||
|
||||
patch(
|
||||
null,
|
||||
vnode,
|
||||
@ -411,12 +440,5 @@ export function createHydrationFunctions(
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user