feat(vapor/hydration): handle component with anchor insertion
This commit is contained in:
parent
9ab8e4c0c9
commit
e5dd701291
|
@ -396,4 +396,46 @@ describe('ssr: element', () => {
|
|||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic child anchor', () => {
|
||||
test('component with element siblings', () => {
|
||||
expect(
|
||||
getCompiledString(`
|
||||
<div>
|
||||
<div/>
|
||||
<Comp1/>
|
||||
<div/>
|
||||
</div>
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"\`<div><div></div>\`)
|
||||
_push("<!--[[-->")
|
||||
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
|
||||
_push("<!--]]-->")
|
||||
_push(\`<div></div></div>\`"
|
||||
`)
|
||||
})
|
||||
|
||||
test('with consecutive components', () => {
|
||||
expect(
|
||||
getCompiledString(`
|
||||
<div>
|
||||
<div/>
|
||||
<Comp1/>
|
||||
<Comp2/>
|
||||
<div/>
|
||||
</div>
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"\`<div><div></div>\`)
|
||||
_push("<!--[[-->")
|
||||
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
|
||||
_push("<!--]]-->")
|
||||
_push("<!--[[-->")
|
||||
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
|
||||
_push("<!--]]-->")
|
||||
_push(\`<div></div></div>\`"
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -255,6 +255,13 @@ export function ssrProcessComponent(
|
|||
node.ssrCodegenNode.arguments.push(`_scopeId`)
|
||||
}
|
||||
|
||||
// `<!--[[-->` marks the start of the dynamic children
|
||||
// Only used in Vapor hydration, VDOM hydration
|
||||
// skips this marker.
|
||||
const needDynamicAnchor = shouldAddDynamicAnchor(parent, node)
|
||||
if (needDynamicAnchor) {
|
||||
context.pushStatement(createCallExpression(`_push`, [`"<!--[[-->"`]))
|
||||
}
|
||||
if (typeof component === 'string') {
|
||||
// static component
|
||||
context.pushStatement(
|
||||
|
@ -265,6 +272,9 @@ export function ssrProcessComponent(
|
|||
// the codegen node is a `renderVNode` call
|
||||
context.pushStatement(node.ssrCodegenNode)
|
||||
}
|
||||
if (needDynamicAnchor) {
|
||||
context.pushStatement(createCallExpression(`_push`, [`"<!--]]-->"`]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,3 +394,39 @@ function clone(v: any): any {
|
|||
return v
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAddDynamicAnchor(
|
||||
parent: { tag?: string; children: TemplateChildNode[] },
|
||||
node: TemplateChildNode,
|
||||
): boolean {
|
||||
if (!parent.tag) return false
|
||||
|
||||
const children = parent.children
|
||||
const len = children.length
|
||||
const index = children.indexOf(node)
|
||||
|
||||
const isStaticElement = (c: TemplateChildNode): boolean =>
|
||||
c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT
|
||||
|
||||
let hasStaticPreviousSibling = false
|
||||
if (index > 0) {
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if (isStaticElement(children[i])) {
|
||||
hasStaticPreviousSibling = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hasStaticNextSibling = false
|
||||
if (hasStaticPreviousSibling && index > -1 && index < len - 1) {
|
||||
for (let i = index + 1; i < len; i++) {
|
||||
if (isStaticElement(children[i])) {
|
||||
hasStaticNextSibling = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasStaticPreviousSibling && hasStaticNextSibling
|
||||
}
|
||||
|
|
|
@ -1843,6 +1843,36 @@ describe('SSR hydration', () => {
|
|||
}
|
||||
})
|
||||
|
||||
describe('dynamic child anchor', () => {
|
||||
test('component with element siblings', () => {
|
||||
const Comp = {
|
||||
render() {
|
||||
return createTextVNode('foo')
|
||||
},
|
||||
}
|
||||
const { vnode, container } = mountWithHydration(
|
||||
`<div><span></span><!--[[-->foo<!--]]--><span></span></div>`,
|
||||
() => h('div', null, [h('span'), h(Comp), h('span')]),
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('with consecutive components', () => {
|
||||
const Comp = {
|
||||
render() {
|
||||
return createTextVNode('foo')
|
||||
},
|
||||
}
|
||||
const { vnode, container } = mountWithHydration(
|
||||
`<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>`,
|
||||
() => h('div', null, [h('span'), h(Comp), h(Comp), h('span')]),
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mismatch handling', () => {
|
||||
test('text node', () => {
|
||||
const { container } = mountWithHydration(`foo`, () => 'bar')
|
||||
|
|
|
@ -111,7 +111,7 @@ export function createHydrationFunctions(
|
|||
o: {
|
||||
patchProp,
|
||||
createText,
|
||||
nextSibling,
|
||||
nextSibling: next,
|
||||
parentNode,
|
||||
remove,
|
||||
insert,
|
||||
|
@ -119,6 +119,19 @@ export function createHydrationFunctions(
|
|||
},
|
||||
} = rendererInternals
|
||||
|
||||
function isDynamicAnchor(node: Node): boolean {
|
||||
return isComment(node) && (node.data === '[[' || node.data === ']]')
|
||||
}
|
||||
|
||||
function nextSibling(node: Node) {
|
||||
let n = next(node)
|
||||
// skip dynamic child anchor
|
||||
if (n && isDynamicAnchor(n)) {
|
||||
n = next(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
const hydrate: RootHydrateFunction = (vnode, container) => {
|
||||
if (!container.hasChildNodes()) {
|
||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
|
@ -145,6 +158,7 @@ export function createHydrationFunctions(
|
|||
slotScopeIds: string[] | null,
|
||||
optimized = false,
|
||||
): Node | null => {
|
||||
if (isDynamicAnchor(node)) node = nextSibling(node)!
|
||||
optimized = optimized || !!vnode.dynamicChildren
|
||||
const isFragmentStart = isComment(node) && node.data === '['
|
||||
const onMismatch = () =>
|
||||
|
@ -451,7 +465,7 @@ export function createHydrationFunctions(
|
|||
|
||||
// The SSRed DOM contains more nodes than it should. Remove them.
|
||||
const cur = next
|
||||
next = next.nextSibling
|
||||
next = nextSibling(next)
|
||||
remove(cur)
|
||||
}
|
||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
|
@ -553,7 +567,7 @@ export function createHydrationFunctions(
|
|||
}
|
||||
}
|
||||
|
||||
return el.nextSibling
|
||||
return nextSibling(el)
|
||||
}
|
||||
|
||||
const hydrateChildren = (
|
||||
|
|
|
@ -239,8 +239,7 @@ describe('Vapor Mode hydration', () => {
|
|||
)
|
||||
})
|
||||
|
||||
// problem is the <!> placeholder does not exist in SSR output
|
||||
test.todo('component with anchor insertion', async () => {
|
||||
test('component with anchor insertion', async () => {
|
||||
const { container, data } = await testHydration(
|
||||
`
|
||||
<template>
|
||||
|
@ -255,14 +254,18 @@ describe('Vapor Mode hydration', () => {
|
|||
Child: `<template>{{ data }}</template>`,
|
||||
},
|
||||
)
|
||||
expect(container.innerHTML).toMatchInlineSnapshot()
|
||||
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||
`"<div><span></span><!--[[-->foo<!--]]--><span></span></div>"`,
|
||||
)
|
||||
|
||||
data.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toMatchInlineSnapshot()
|
||||
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||
`"<div><span></span><!--[[-->bar<!--]]--><span></span></div>"`,
|
||||
)
|
||||
})
|
||||
|
||||
test.todo('consecutive component with anchor insertion', async () => {
|
||||
test('consecutive component with anchor insertion', async () => {
|
||||
const { container, data } = await testHydration(
|
||||
`<template>
|
||||
<div>
|
||||
|
@ -277,11 +280,15 @@ describe('Vapor Mode hydration', () => {
|
|||
Child: `<template>{{ data }}</template>`,
|
||||
},
|
||||
)
|
||||
expect(container.innerHTML).toMatchInlineSnapshot()
|
||||
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||
`"<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
|
||||
)
|
||||
|
||||
data.value = 'bar'
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toMatchInlineSnapshot()
|
||||
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||
`"<div><span></span><!--[[-->bar<!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
|
||||
)
|
||||
})
|
||||
|
||||
test.todo('if')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { warn } from '@vue/runtime-dom'
|
||||
import {
|
||||
type Anchor,
|
||||
insertionAnchor,
|
||||
insertionParent,
|
||||
resetInsertionState,
|
||||
|
@ -36,12 +37,6 @@ export function withHydration(container: ParentNode, fn: () => void): void {
|
|||
export let adoptTemplate: (node: Node, template: string) => Node | null
|
||||
export let locateHydrationNode: () => void
|
||||
|
||||
type Anchor = Comment & {
|
||||
// cached matching fragment start to avoid repeated traversal
|
||||
// on nested fragments
|
||||
$fs?: Anchor
|
||||
}
|
||||
|
||||
const isComment = (node: Node, data: string): node is Anchor =>
|
||||
node.nodeType === 8 && (node as Comment).data === data
|
||||
|
||||
|
@ -77,41 +72,48 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
|||
|
||||
function locateHydrationNodeImpl() {
|
||||
let node: Node | null
|
||||
|
||||
// prepend / firstChild
|
||||
if (insertionAnchor === 0) {
|
||||
node = child(insertionParent!)
|
||||
} else {
|
||||
node = insertionAnchor
|
||||
? insertionAnchor.previousSibling
|
||||
: insertionParent
|
||||
? insertionParent.lastChild
|
||||
: currentHydrationNode
|
||||
|
||||
if (node && isComment(node, ']')) {
|
||||
// fragment backward search
|
||||
if (node.$fs) {
|
||||
// already cached matching fragment start
|
||||
node = node.$fs
|
||||
} else {
|
||||
let cur: Node | null = node
|
||||
let curFragEnd = node
|
||||
let fragDepth = 0
|
||||
node = null
|
||||
while (cur) {
|
||||
cur = cur.previousSibling
|
||||
if (cur) {
|
||||
if (isComment(cur, '[')) {
|
||||
curFragEnd.$fs = cur
|
||||
if (!fragDepth) {
|
||||
node = cur
|
||||
break
|
||||
} else {
|
||||
fragDepth--
|
||||
// dynamic child anchor `<!--[[-->`
|
||||
if (insertionAnchor && isDynamicStart(insertionAnchor)) {
|
||||
const anchor = (insertionParent!.lds = insertionParent!.lds
|
||||
? // continuous dynamic children, the next dynamic start must exist
|
||||
locateNextDynamicStart(insertionParent!.lds)!
|
||||
: insertionAnchor)
|
||||
node = anchor.nextSibling
|
||||
} else {
|
||||
node = insertionAnchor
|
||||
? insertionAnchor.previousSibling
|
||||
: insertionParent
|
||||
? insertionParent.lastChild
|
||||
: currentHydrationNode
|
||||
if (node && isComment(node, ']')) {
|
||||
// fragment backward search
|
||||
if (node.$fs) {
|
||||
// already cached matching fragment start
|
||||
node = node.$fs
|
||||
} else {
|
||||
let cur: Node | null = node
|
||||
let curFragEnd = node
|
||||
let fragDepth = 0
|
||||
node = null
|
||||
while (cur) {
|
||||
cur = cur.previousSibling
|
||||
if (cur) {
|
||||
if (isComment(cur, '[')) {
|
||||
curFragEnd.$fs = cur
|
||||
if (!fragDepth) {
|
||||
node = cur
|
||||
break
|
||||
} else {
|
||||
fragDepth--
|
||||
}
|
||||
} else if (isComment(cur, ']')) {
|
||||
curFragEnd = cur
|
||||
fragDepth++
|
||||
}
|
||||
} else if (isComment(cur, ']')) {
|
||||
curFragEnd = cur
|
||||
fragDepth++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,3 +129,32 @@ function locateHydrationNodeImpl() {
|
|||
resetInsertionState()
|
||||
currentHydrationNode = node
|
||||
}
|
||||
|
||||
function isDynamicStart(node: Node): node is Anchor {
|
||||
return isComment(node, '[[')
|
||||
}
|
||||
|
||||
function locateNextDynamicStart(anchor: Anchor): Anchor | undefined {
|
||||
let cur: Node | null = anchor
|
||||
let end = null
|
||||
let depth = 0
|
||||
while (cur) {
|
||||
cur = cur.nextSibling
|
||||
if (cur) {
|
||||
if (isComment(cur, '[[')) {
|
||||
depth++
|
||||
} else if (isComment(cur, ']]')) {
|
||||
if (!depth) {
|
||||
end = cur
|
||||
break
|
||||
} else {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (end) {
|
||||
return end!.nextSibling as Anchor
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
export let insertionParent: ParentNode | undefined
|
||||
export let insertionAnchor: Node | 0 | undefined
|
||||
export let insertionParent:
|
||||
| (ParentNode & {
|
||||
// cached the last dynamic start anchor
|
||||
lds?: Anchor
|
||||
})
|
||||
| undefined
|
||||
export let insertionAnchor: Node | 0 | undefined | null
|
||||
|
||||
/**
|
||||
* This function is called before a block type that requires insertion
|
||||
|
@ -14,3 +19,13 @@ export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
|
|||
export function resetInsertionState(): void {
|
||||
insertionParent = insertionAnchor = undefined
|
||||
}
|
||||
|
||||
export function setInsertionAnchor(anchor: Node | null): void {
|
||||
insertionAnchor = anchor
|
||||
}
|
||||
|
||||
export type Anchor = Comment & {
|
||||
// cached matching fragment start to avoid repeated traversal
|
||||
// on nested fragments
|
||||
$fs?: Anchor
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue