From 17f7ff0e1bb7a0141d90687f521db21dd63f6965 Mon Sep 17 00:00:00 2001 From: Rairn <958414905@qq.com> Date: Thu, 8 Dec 2022 22:21:51 +0800 Subject: [PATCH] fix(ssr): fix hydration mismatch warning about mutiple continuous text vnodes (#7285) --- .../runtime-core/__tests__/hydration.spec.ts | 19 +++++ packages/runtime-core/src/hydration.ts | 73 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index fece1a33f9d..efa2476f6a6 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -136,6 +136,15 @@ describe('SSR hydration', () => { expect(container.innerHTML).toBe(`
bar
`) }) + // #7285 + test('element with multiple continuous text vnodes', async () => { + // should no mismatch warning + const { container } = mountWithHydration('
foo
', () => + h('div', ['fo', 'o']) + ) + expect(container.textContent).toBe('foo') + }) + test('element with elements children', async () => { const msg = ref('foo') const fn = vi.fn() @@ -224,6 +233,16 @@ describe('SSR hydration', () => { ) }) + // #7285 + test('Fragment (multiple continuous text vnodes)', async () => { + // should no mismatch warning + const { container } = mountWithHydration('foo', () => [ + 'fo', + 'o' + ]) + expect(container.textContent).toBe('foo') + }) + test('Teleport', async () => { const msg = ref('foo') const fn = vi.fn() diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 2170a9192cf..9392f8048c4 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -428,11 +428,78 @@ export function createHydrationFunctions( optimized = optimized || !!parentVNode.dynamicChildren const children = parentVNode.children as VNode[] const l = children.length + const continuousTextVnodes = [] + const getVnode = (i: number) => + optimized ? children[i] : (children[i] = normalizeVNode(children[i])) let hasWarned = false for (let i = 0; i < l; i++) { - const vnode = optimized - ? children[i] - : (children[i] = normalizeVNode(children[i])) + let vnode = getVnode(i) + + // #7285 - multiple continuous text vnodes in children can cause hydration + // failure because the server rendered HTML just contain one text node + if ( + vnode.type === Text && + node && + node.nodeType === DOMNodeTypes.TEXT && + vnode.children !== (node as Text).data + ) { + const nextVnode = getVnode(i + 1) + // for final merging into one text + continuousTextVnodes.push(vnode) + // if the next vnode is also text, it means the children has multiple continuous + // text vnodes, we need to merge them into one text to avoid hydration failure + if (nextVnode.type === Text) { + patch( + null, + vnode, + container, + node, + parentComponent, + parentSuspense, + isSVGContainer(container), + slotScopeIds + ) + continue + } else if (continuousTextVnodes.length > 1) { + const text = continuousTextVnodes.map(v => v.children).join('') + if ((node as Text).data !== text) { + hasMismatch = true + __DEV__ && + warn( + `Hydration text mismatch:` + + `\n- Client: ${JSON.stringify((node as Text).data)}` + + `\n- Server: ${text}` + ) + } + // insert the last text vnode to the container + patch( + null, + vnode, + container, + node, + parentComponent, + parentSuspense, + isSVGContainer(container), + slotScopeIds + ) + + const nextNode = nextSibling(node) + // because the node's text has been inserted to the container, + // so we need to remove it + remove(node) + node = nextNode + vnode = nextVnode + continuousTextVnodes.length = 0 + i++ + if (i === l) { + continue + } + } else { + // if only one text vnode, do nothing + continuousTextVnodes.length = 0 + } + } + if (node) { node = hydrateNode( node,