From: daiwei Date: Mon, 21 Apr 2025 07:38:50 +0000 (+0800) Subject: feat(vapor/hydration): handle component with anchor insertion X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e5dd701291745af6d67b97857597d73cc631117f;p=thirdparty%2Fvuejs%2Fcore.git feat(vapor/hydration): handle component with anchor insertion --- diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index f1d509acfb..97601ae9ab 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -396,4 +396,46 @@ describe('ssr: element', () => { `) }) }) + + describe('dynamic child anchor', () => { + test('component with element siblings', () => { + expect( + getCompiledString(` +
+
+ +
+
+ `), + ).toMatchInlineSnapshot(` + "\`
\`) + _push("") + _push(_ssrRenderComponent(_component_Comp1, null, null, _parent)) + _push("") + _push(\`
\`" + `) + }) + + test('with consecutive components', () => { + expect( + getCompiledString(` +
+
+ + +
+
+ `), + ).toMatchInlineSnapshot(` + "\`
\`) + _push("") + _push(_ssrRenderComponent(_component_Comp1, null, null, _parent)) + _push("") + _push("") + _push(_ssrRenderComponent(_component_Comp2, null, null, _parent)) + _push("") + _push(\`
\`" + `) + }) + }) }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index cad1ee8102..a130dc427f 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -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 +} diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 56011d0635..07a6504b50 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -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( + `
foo
`, + () => 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( + `
foofoo
`, + () => 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') diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index ef6f1918c3..13f900a948 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -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 = ( diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 6ba2bf895f..def3c9d924 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -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( `