From dd2d25fee18f5ff0d485d6f95d69fa3a94c0fa2d Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 6 Mar 2020 15:39:54 -0500 Subject: [PATCH] test(ssr): test for hydration mismatch handling --- .../compiler-core/__tests__/hydration.spec.ts | 45 +++++++++++++++++-- packages/runtime-core/src/hydration.ts | 30 ++++++++++--- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/packages/compiler-core/__tests__/hydration.spec.ts b/packages/compiler-core/__tests__/hydration.spec.ts index 37672db06..6d311bfe2 100644 --- a/packages/compiler-core/__tests__/hydration.spec.ts +++ b/packages/compiler-core/__tests__/hydration.spec.ts @@ -8,6 +8,7 @@ import { createStaticVNode } from '@vue/runtime-dom' import { renderToString } from '@vue/server-renderer' +import { mockWarn } from '@vue/shared' function mountWithHydration(html: string, render: () => any) { const container = document.createElement('div') @@ -268,12 +269,48 @@ describe('SSR hydration', () => { }) describe('mismatch handling', () => { - test('text', () => {}) + mockWarn() - test('not enough children', () => {}) + test('text node', () => { + const { container } = mountWithHydration(`foo`, () => 'bar') + expect(container.textContent).toBe('bar') + expect(`Hydration text mismatch`).toHaveBeenWarned() + }) - test('too many children', () => {}) + test('element text content', () => { + const { container } = mountWithHydration(`
foo
`, () => + h('div', 'bar') + ) + expect(container.innerHTML).toBe('
bar
') + expect(`Hydration text content mismatch in
`).toHaveBeenWarned() + }) - test('complete mismatch', () => {}) + test('not enough children', () => { + const { container } = mountWithHydration(`
`, () => + h('div', [h('span', 'foo'), h('span', 'bar')]) + ) + expect(container.innerHTML).toBe( + '
foobar
' + ) + expect(`Hydration children mismatch in
`).toHaveBeenWarned() + }) + + test('too many children', () => { + const { container } = mountWithHydration( + `
foobar
`, + () => h('div', [h('span', 'foo')]) + ) + expect(container.innerHTML).toBe('
foo
') + expect(`Hydration children mismatch in
`).toHaveBeenWarned() + }) + + test('complete mismatch', () => { + const { container } = mountWithHydration( + `
foobar
`, + () => h('div', [h('div', 'foo'), h('p', 'bar')]) + ) + expect(container.innerHTML).toBe('
foo

bar

') + expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2) + }) }) }) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index efb11cf71..c38ea9497 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -94,7 +94,10 @@ export function createHydrationFunctions({ return hydrateFragment(node, vnode, parentComponent, optimized) default: if (shapeFlag & ShapeFlags.ELEMENT) { - if (domType !== DOMNodeTypes.ELEMENT) { + if ( + domType !== DOMNodeTypes.ELEMENT || + vnode.type !== (node as Element).tagName.toLowerCase() + ) { return handleMismtach(node, vnode, parentComponent) } return hydrateElement( @@ -176,20 +179,32 @@ export function createHydrationFunctions({ parentComponent, optimized ) + let hasWarned = false while (next) { hasMismatch = true - __DEV__ && + if (__DEV__ && !hasWarned) { warn( - `Hydration children mismatch: ` + + `Hydration children mismatch in <${vnode.type as string}>: ` + `server rendered element contains more child nodes than client vdom.` ) + hasWarned = true + } // The SSRed DOM contains more nodes than it should. Remove them. const cur = next next = next.nextSibling el.removeChild(cur) } } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { - el.textContent = vnode.children as string + if (el.textContent !== vnode.children) { + hasMismatch = true + __DEV__ && + warn( + `Hydration text content mismatch in <${vnode.type as string}>:\n` + + `- Client: ${el.textContent}\n` + + `- Server: ${vnode.children as string}` + ) + el.textContent = vnode.children as string + } } } return el.nextSibling @@ -205,6 +220,7 @@ export function createHydrationFunctions({ optimized = optimized || vnode.dynamicChildren !== null const children = vnode.children as VNode[] const l = children.length + let hasWarned = false for (let i = 0; i < l; i++) { const vnode = optimized ? children[i] @@ -213,11 +229,13 @@ export function createHydrationFunctions({ node = hydrateNode(node, vnode, parentComponent, optimized) } else { hasMismatch = true - __DEV__ && + if (__DEV__ && !hasWarned) { warn( - `Hydration children mismatch: ` + + `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` + `server rendered element contains fewer child nodes than client vdom.` ) + hasWarned = true + } // the SSRed DOM didn't contain enough nodes. Mount the missing ones. patch(null, vnode, container) }