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`)
|
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') {
|
if (typeof component === 'string') {
|
||||||
// static component
|
// static component
|
||||||
context.pushStatement(
|
context.pushStatement(
|
||||||
|
@ -265,6 +272,9 @@ export function ssrProcessComponent(
|
||||||
// the codegen node is a `renderVNode` call
|
// the codegen node is a `renderVNode` call
|
||||||
context.pushStatement(node.ssrCodegenNode)
|
context.pushStatement(node.ssrCodegenNode)
|
||||||
}
|
}
|
||||||
|
if (needDynamicAnchor) {
|
||||||
|
context.pushStatement(createCallExpression(`_push`, [`"<!--]]-->"`]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,3 +394,39 @@ function clone(v: any): any {
|
||||||
return v
|
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', () => {
|
describe('mismatch handling', () => {
|
||||||
test('text node', () => {
|
test('text node', () => {
|
||||||
const { container } = mountWithHydration(`foo`, () => 'bar')
|
const { container } = mountWithHydration(`foo`, () => 'bar')
|
||||||
|
|
|
@ -111,7 +111,7 @@ export function createHydrationFunctions(
|
||||||
o: {
|
o: {
|
||||||
patchProp,
|
patchProp,
|
||||||
createText,
|
createText,
|
||||||
nextSibling,
|
nextSibling: next,
|
||||||
parentNode,
|
parentNode,
|
||||||
remove,
|
remove,
|
||||||
insert,
|
insert,
|
||||||
|
@ -119,6 +119,19 @@ export function createHydrationFunctions(
|
||||||
},
|
},
|
||||||
} = rendererInternals
|
} = 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) => {
|
const hydrate: RootHydrateFunction = (vnode, container) => {
|
||||||
if (!container.hasChildNodes()) {
|
if (!container.hasChildNodes()) {
|
||||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||||
|
@ -145,6 +158,7 @@ export function createHydrationFunctions(
|
||||||
slotScopeIds: string[] | null,
|
slotScopeIds: string[] | null,
|
||||||
optimized = false,
|
optimized = false,
|
||||||
): Node | null => {
|
): Node | null => {
|
||||||
|
if (isDynamicAnchor(node)) node = nextSibling(node)!
|
||||||
optimized = optimized || !!vnode.dynamicChildren
|
optimized = optimized || !!vnode.dynamicChildren
|
||||||
const isFragmentStart = isComment(node) && node.data === '['
|
const isFragmentStart = isComment(node) && node.data === '['
|
||||||
const onMismatch = () =>
|
const onMismatch = () =>
|
||||||
|
@ -451,7 +465,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 = nextSibling(next)
|
||||||
remove(cur)
|
remove(cur)
|
||||||
}
|
}
|
||||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||||
|
@ -553,7 +567,7 @@ export function createHydrationFunctions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return el.nextSibling
|
return nextSibling(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateChildren = (
|
const hydrateChildren = (
|
||||||
|
|
|
@ -239,8 +239,7 @@ describe('Vapor Mode hydration', () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// problem is the <!> placeholder does not exist in SSR output
|
test('component with anchor insertion', async () => {
|
||||||
test.todo('component with anchor insertion', async () => {
|
|
||||||
const { container, data } = await testHydration(
|
const { container, data } = await testHydration(
|
||||||
`
|
`
|
||||||
<template>
|
<template>
|
||||||
|
@ -255,14 +254,18 @@ describe('Vapor Mode hydration', () => {
|
||||||
Child: `<template>{{ data }}</template>`,
|
Child: `<template>{{ data }}</template>`,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot()
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span><!--[[-->foo<!--]]--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
|
||||||
data.value = 'bar'
|
data.value = 'bar'
|
||||||
await nextTick()
|
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(
|
const { container, data } = await testHydration(
|
||||||
`<template>
|
`<template>
|
||||||
<div>
|
<div>
|
||||||
|
@ -277,11 +280,15 @@ describe('Vapor Mode hydration', () => {
|
||||||
Child: `<template>{{ data }}</template>`,
|
Child: `<template>{{ data }}</template>`,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot()
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
|
||||||
|
)
|
||||||
|
|
||||||
data.value = 'bar'
|
data.value = 'bar'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot()
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
||||||
|
`"<div><span></span><!--[[-->bar<!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.todo('if')
|
test.todo('if')
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { warn } from '@vue/runtime-dom'
|
import { warn } from '@vue/runtime-dom'
|
||||||
import {
|
import {
|
||||||
|
type Anchor,
|
||||||
insertionAnchor,
|
insertionAnchor,
|
||||||
insertionParent,
|
insertionParent,
|
||||||
resetInsertionState,
|
resetInsertionState,
|
||||||
|
@ -36,12 +37,6 @@ export function withHydration(container: ParentNode, fn: () => void): void {
|
||||||
export let adoptTemplate: (node: Node, template: string) => Node | null
|
export let adoptTemplate: (node: Node, template: string) => Node | null
|
||||||
export let locateHydrationNode: () => void
|
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 =>
|
const isComment = (node: Node, data: string): node is Anchor =>
|
||||||
node.nodeType === 8 && (node as Comment).data === data
|
node.nodeType === 8 && (node as Comment).data === data
|
||||||
|
|
||||||
|
@ -77,17 +72,23 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
||||||
|
|
||||||
function locateHydrationNodeImpl() {
|
function locateHydrationNodeImpl() {
|
||||||
let node: Node | null
|
let node: Node | null
|
||||||
|
|
||||||
// prepend / firstChild
|
// prepend / firstChild
|
||||||
if (insertionAnchor === 0) {
|
if (insertionAnchor === 0) {
|
||||||
node = child(insertionParent!)
|
node = child(insertionParent!)
|
||||||
|
} else {
|
||||||
|
// 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 {
|
} else {
|
||||||
node = insertionAnchor
|
node = insertionAnchor
|
||||||
? insertionAnchor.previousSibling
|
? insertionAnchor.previousSibling
|
||||||
: insertionParent
|
: insertionParent
|
||||||
? insertionParent.lastChild
|
? insertionParent.lastChild
|
||||||
: currentHydrationNode
|
: currentHydrationNode
|
||||||
|
|
||||||
if (node && isComment(node, ']')) {
|
if (node && isComment(node, ']')) {
|
||||||
// fragment backward search
|
// fragment backward search
|
||||||
if (node.$fs) {
|
if (node.$fs) {
|
||||||
|
@ -118,6 +119,7 @@ function locateHydrationNodeImpl() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (__DEV__ && !node) {
|
if (__DEV__ && !node) {
|
||||||
// TODO more info
|
// TODO more info
|
||||||
|
@ -127,3 +129,32 @@ function locateHydrationNodeImpl() {
|
||||||
resetInsertionState()
|
resetInsertionState()
|
||||||
currentHydrationNode = node
|
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 insertionParent:
|
||||||
export let insertionAnchor: Node | 0 | undefined
|
| (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
|
* 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 {
|
export function resetInsertionState(): void {
|
||||||
insertionParent = insertionAnchor = undefined
|
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