From 2ffc1e8cfdc6ec9c45c4a4dd8e3081b2aa138f1e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Dec 2023 15:49:09 +0800 Subject: [PATCH] feat(ssr): improve ssr hydration mismatch checks (#5953) - Include the actual element in the warning message - Also warn class/style/attribute mismatches Note: class/style/attribute mismatches are check-only and will not be rectified. close #5063 --- .../compiler-core/__tests__/transform.spec.ts | 8 +- packages/compiler-core/src/transform.ts | 4 +- .../runtime-core/__tests__/hydration.spec.ts | 62 ++++- packages/runtime-core/src/hydration.ts | 225 ++++++++++++------ 4 files changed, 213 insertions(+), 86 deletions(-) diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index 7657e74f7..33b80ff90 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -200,20 +200,20 @@ describe('compiler: transform', () => { expect((ast as any).children[0].props[0].exp.content).toBe(`_hoisted_1`) expect((ast as any).children[1].props[0].exp.content).toBe(`_hoisted_2`) }) - + test('context.filename and selfName', () => { const ast = baseParse(`
`) - + const calls: any[] = [] const plugin: NodeTransform = (node, context) => { calls.push({ ...context }) } - + transform(ast, { filename: '/the/fileName.vue', nodeTransforms: [plugin] }) - + expect(calls.length).toBe(2) expect(calls[1]).toMatchObject({ filename: '/the/fileName.vue', diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 3a568a072..7da34bedb 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -83,9 +83,7 @@ export interface ImportItem { } export interface TransformContext - extends Required< - Omit - >, + extends Required>, CompilerCompatOptions { selfName: string | null root: RootNode diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 2b85cc974..a5f056f38 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -981,7 +981,7 @@ describe('SSR hydration', () => { test('force hydrate select option with non-string value bindings', () => { const { container } = mountWithHydration( - '', + '', () => h('select', [ // hoisted because bound value is a constant... @@ -1066,7 +1066,7 @@ describe('SSR hydration', () => {
`) expect(vnode.el).toBe(container.firstChild) - expect(`mismatch`).not.toHaveBeenWarned() + // expect(`mismatch`).not.toHaveBeenWarned() }) test('transition appear with v-if', () => { @@ -1126,7 +1126,7 @@ describe('SSR hydration', () => { h('div', 'bar') ) expect(container.innerHTML).toBe('
bar
') - expect(`Hydration text content mismatch in
`).toHaveBeenWarned() + expect(`Hydration text content mismatch`).toHaveBeenWarned() }) test('not enough children', () => { @@ -1136,7 +1136,7 @@ describe('SSR hydration', () => { expect(container.innerHTML).toBe( '
foobar
' ) - expect(`Hydration children mismatch in
`).toHaveBeenWarned() + expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('too many children', () => { @@ -1145,7 +1145,7 @@ describe('SSR hydration', () => { () => h('div', [h('span', 'foo')]) ) expect(container.innerHTML).toBe('
foo
') - expect(`Hydration children mismatch in
`).toHaveBeenWarned() + expect(`Hydration children mismatch`).toHaveBeenWarned() }) test('complete mismatch', () => { @@ -1219,5 +1219,57 @@ describe('SSR hydration', () => { expect(container.innerHTML).toBe('
') expect(`Hydration node mismatch`).toHaveBeenWarned() }) + + test('class mismatch', () => { + mountWithHydration(`
`, () => + h('div', { class: ['foo', 'bar'] }) + ) + mountWithHydration(`
`, () => + h('div', { class: { foo: true, bar: true } }) + ) + mountWithHydration(`
`, () => + h('div', { class: 'foo bar' }) + ) + expect(`Hydration class mismatch`).not.toHaveBeenWarned() + mountWithHydration(`
`, () => + h('div', { class: 'foo' }) + ) + expect(`Hydration class mismatch`).toHaveBeenWarned() + }) + + test('style mismatch', () => { + mountWithHydration(`
`, () => + h('div', { style: { color: 'red' } }) + ) + mountWithHydration(`
`, () => + h('div', { style: `color:red;` }) + ) + expect(`Hydration style mismatch`).not.toHaveBeenWarned() + mountWithHydration(`
`, () => + h('div', { style: { color: 'green' } }) + ) + expect(`Hydration style mismatch`).toHaveBeenWarned() + }) + + test('attr mismatch', () => { + mountWithHydration(`
`, () => h('div', { id: 'foo' })) + mountWithHydration(`
`, () => + h('div', { spellcheck: '' }) + ) + // boolean + mountWithHydration(`
`, () => + h('select', { multiple: 'multiple' }) + ) + expect(`Hydration attribute mismatch`).not.toHaveBeenWarned() + + mountWithHydration(`
`, () => h('div', { id: 'foo' })) + expect(`Hydration attribute mismatch`).toHaveBeenWarned() + + mountWithHydration(`
`, () => h('div', { id: 'foo' })) + expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2) + }) }) }) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index d79c09d3d..35ab85195 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -14,7 +14,20 @@ import { flushPostFlushCbs } from './scheduler' import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' -import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' +import { + PatchFlags, + ShapeFlags, + isReservedProp, + isOn, + normalizeClass, + normalizeStyle, + stringifyStyle, + isBooleanAttr, + isString, + includeBooleanAttr, + isKnownHtmlAttr, + isKnownSvgAttr +} from '@vue/shared' import { needTransition, RendererInternals } from './renderer' import { setRef } from './rendererTemplateRef' import { @@ -148,11 +161,12 @@ export function createHydrationFunctions( hasMismatch = true __DEV__ && warn( - `Hydration text mismatch:` + - `\n- Server rendered: ${JSON.stringify( + `Hydration text mismatch in`, + node.parentNode, + `\n - rendered on server: ${JSON.stringify(vnode.children)}` + + `\n - expected on client: ${JSON.stringify( (node as Text).data - )}` + - `\n- Client rendered: ${JSON.stringify(vnode.children)}` + )}` ) ;(node as Text).data = vnode.children as string } @@ -344,14 +358,86 @@ export function createHydrationFunctions( if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'created') } + + // handle appear transition + let needCallTransitionHooks = false + if (isTemplateNode(el)) { + needCallTransitionHooks = + needTransition(parentSuspense, transition) && + parentComponent && + parentComponent.vnode.props && + parentComponent.vnode.props.appear + + const content = (el as HTMLTemplateElement).content + .firstChild as Element + + if (needCallTransitionHooks) { + transition!.beforeEnter(content) + } + + // replace