From e5dd701291745af6d67b97857597d73cc631117f Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 21 Apr 2025 15:38:50 +0800 Subject: [PATCH] feat(vapor/hydration): handle component with anchor insertion --- .../compiler-ssr/__tests__/ssrElement.spec.ts | 42 +++++++ .../src/transforms/ssrTransformComponent.ts | 46 ++++++++ .../runtime-core/__tests__/hydration.spec.ts | 30 +++++ packages/runtime-core/src/hydration.ts | 20 +++- .../runtime-vapor/__tests__/hydration.spec.ts | 21 ++-- packages/runtime-vapor/src/dom/hydration.ts | 103 ++++++++++++------ packages/runtime-vapor/src/insertionState.ts | 19 +++- 7 files changed, 233 insertions(+), 48 deletions(-) diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index f1d509acf..97601ae9a 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 cad1ee810..a130dc427 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 56011d063..07a6504b5 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 ef6f1918c..13f900a94 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 6ba2bf895..def3c9d92 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( `