|
|
@@ -405,18 +405,20 @@ export function createHydrationFunctions(
|
|
|
)
|
|
|
let hasWarned = false
|
|
|
while (next) {
|
|
|
- if (
|
|
|
- (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
- !hasWarned
|
|
|
- ) {
|
|
|
- warn(
|
|
|
- `Hydration children mismatch on`,
|
|
|
- el,
|
|
|
- `\nServer rendered element contains more child nodes than client vdom.`,
|
|
|
- )
|
|
|
- hasWarned = true
|
|
|
+ if (!isMismatchAllowed(el, MismatchTypes.CHILDREN)) {
|
|
|
+ if (
|
|
|
+ (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
+ !hasWarned
|
|
|
+ ) {
|
|
|
+ warn(
|
|
|
+ `Hydration children mismatch on`,
|
|
|
+ el,
|
|
|
+ `\nServer rendered element contains more child nodes than client vdom.`,
|
|
|
+ )
|
|
|
+ hasWarned = true
|
|
|
+ }
|
|
|
+ logMismatchError()
|
|
|
}
|
|
|
- logMismatchError()
|
|
|
|
|
|
// The SSRed DOM contains more nodes than it should. Remove them.
|
|
|
const cur = next
|
|
|
@@ -425,14 +427,16 @@ export function createHydrationFunctions(
|
|
|
}
|
|
|
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
|
|
if (el.textContent !== vnode.children) {
|
|
|
- ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
- warn(
|
|
|
- `Hydration text content mismatch on`,
|
|
|
- el,
|
|
|
- `\n - rendered on server: ${el.textContent}` +
|
|
|
- `\n - expected on client: ${vnode.children as string}`,
|
|
|
- )
|
|
|
- logMismatchError()
|
|
|
+ if (!isMismatchAllowed(el, MismatchTypes.TEXT)) {
|
|
|
+ ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
+ warn(
|
|
|
+ `Hydration text content mismatch on`,
|
|
|
+ el,
|
|
|
+ `\n - rendered on server: ${el.textContent}` +
|
|
|
+ `\n - expected on client: ${vnode.children as string}`,
|
|
|
+ )
|
|
|
+ logMismatchError()
|
|
|
+ }
|
|
|
|
|
|
el.textContent = vnode.children as string
|
|
|
}
|
|
|
@@ -562,18 +566,20 @@ export function createHydrationFunctions(
|
|
|
// because server rendered HTML won't contain a text node
|
|
|
insert((vnode.el = createText('')), container)
|
|
|
} else {
|
|
|
- if (
|
|
|
- (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
- !hasWarned
|
|
|
- ) {
|
|
|
- warn(
|
|
|
- `Hydration children mismatch on`,
|
|
|
- container,
|
|
|
- `\nServer rendered element contains fewer child nodes than client vdom.`,
|
|
|
- )
|
|
|
- hasWarned = true
|
|
|
+ if (!isMismatchAllowed(container, MismatchTypes.CHILDREN)) {
|
|
|
+ if (
|
|
|
+ (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
+ !hasWarned
|
|
|
+ ) {
|
|
|
+ warn(
|
|
|
+ `Hydration children mismatch on`,
|
|
|
+ container,
|
|
|
+ `\nServer rendered element contains fewer child nodes than client vdom.`,
|
|
|
+ )
|
|
|
+ hasWarned = true
|
|
|
+ }
|
|
|
+ logMismatchError()
|
|
|
}
|
|
|
- logMismatchError()
|
|
|
|
|
|
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
|
|
|
patch(
|
|
|
@@ -637,19 +643,21 @@ export function createHydrationFunctions(
|
|
|
slotScopeIds: string[] | null,
|
|
|
isFragment: boolean,
|
|
|
): Node | null => {
|
|
|
- ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
- warn(
|
|
|
- `Hydration node mismatch:\n- rendered on server:`,
|
|
|
- node,
|
|
|
- node.nodeType === DOMNodeTypes.TEXT
|
|
|
- ? `(text)`
|
|
|
- : isComment(node) && node.data === '['
|
|
|
- ? `(start of fragment)`
|
|
|
- : ``,
|
|
|
- `\n- expected on client:`,
|
|
|
- vnode.type,
|
|
|
- )
|
|
|
- logMismatchError()
|
|
|
+ if (!isMismatchAllowed(node.parentElement!, MismatchTypes.CHILDREN)) {
|
|
|
+ ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
|
|
+ warn(
|
|
|
+ `Hydration node mismatch:\n- rendered on server:`,
|
|
|
+ node,
|
|
|
+ node.nodeType === DOMNodeTypes.TEXT
|
|
|
+ ? `(text)`
|
|
|
+ : isComment(node) && node.data === '['
|
|
|
+ ? `(start of fragment)`
|
|
|
+ : ``,
|
|
|
+ `\n- expected on client:`,
|
|
|
+ vnode.type,
|
|
|
+ )
|
|
|
+ logMismatchError()
|
|
|
+ }
|
|
|
|
|
|
vnode.el = null
|
|
|
|
|
|
@@ -747,7 +755,7 @@ function propHasMismatch(
|
|
|
vnode: VNode,
|
|
|
instance: ComponentInternalInstance | null,
|
|
|
): boolean {
|
|
|
- let mismatchType: string | undefined
|
|
|
+ let mismatchType: MismatchTypes | undefined
|
|
|
let mismatchKey: string | undefined
|
|
|
let actual: string | boolean | null | undefined
|
|
|
let expected: string | boolean | null | undefined
|
|
|
@@ -757,7 +765,8 @@ function propHasMismatch(
|
|
|
actual = el.getAttribute('class')
|
|
|
expected = normalizeClass(clientValue)
|
|
|
if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
|
|
|
- mismatchType = mismatchKey = `class`
|
|
|
+ mismatchType = MismatchTypes.CLASS
|
|
|
+ mismatchKey = `class`
|
|
|
}
|
|
|
} else if (key === 'style') {
|
|
|
// style might be in different order, but that doesn't affect cascade
|
|
|
@@ -782,7 +791,8 @@ function propHasMismatch(
|
|
|
}
|
|
|
|
|
|
if (!isMapEqual(actualMap, expectedMap)) {
|
|
|
- mismatchType = mismatchKey = 'style'
|
|
|
+ mismatchType = MismatchTypes.STYLE
|
|
|
+ mismatchKey = 'style'
|
|
|
}
|
|
|
} else if (
|
|
|
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
|
|
|
@@ -808,15 +818,15 @@ function propHasMismatch(
|
|
|
: false
|
|
|
}
|
|
|
if (actual !== expected) {
|
|
|
- mismatchType = `attribute`
|
|
|
+ mismatchType = MismatchTypes.ATTRIBUTE
|
|
|
mismatchKey = key
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (mismatchType) {
|
|
|
+ if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) {
|
|
|
const format = (v: any) =>
|
|
|
v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
|
|
|
- const preSegment = `Hydration ${mismatchType} mismatch on`
|
|
|
+ const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`
|
|
|
const postSegment =
|
|
|
`\n - rendered on server: ${format(actual)}` +
|
|
|
`\n - expected on client: ${format(expected)}` +
|
|
|
@@ -898,3 +908,48 @@ function resolveCssVars(
|
|
|
resolveCssVars(instance.parent, instance.vnode, expectedMap)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+const allowMismatchAttr = 'data-allow-mismatch'
|
|
|
+
|
|
|
+enum MismatchTypes {
|
|
|
+ TEXT = 0,
|
|
|
+ CHILDREN = 1,
|
|
|
+ CLASS = 2,
|
|
|
+ STYLE = 3,
|
|
|
+ ATTRIBUTE = 4,
|
|
|
+}
|
|
|
+
|
|
|
+const MismatchTypeString: Record<MismatchTypes, string> = {
|
|
|
+ [MismatchTypes.TEXT]: 'text',
|
|
|
+ [MismatchTypes.CHILDREN]: 'children',
|
|
|
+ [MismatchTypes.CLASS]: 'class',
|
|
|
+ [MismatchTypes.STYLE]: 'style',
|
|
|
+ [MismatchTypes.ATTRIBUTE]: 'attribute',
|
|
|
+} as const
|
|
|
+
|
|
|
+function isMismatchAllowed(
|
|
|
+ el: Element | null,
|
|
|
+ allowedType: MismatchTypes,
|
|
|
+): boolean {
|
|
|
+ if (
|
|
|
+ allowedType === MismatchTypes.TEXT ||
|
|
|
+ allowedType === MismatchTypes.CHILDREN
|
|
|
+ ) {
|
|
|
+ while (el && !el.hasAttribute(allowMismatchAttr)) {
|
|
|
+ el = el.parentElement
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const allowedAttr = el && el.getAttribute(allowMismatchAttr)
|
|
|
+ if (allowedAttr == null) {
|
|
|
+ return false
|
|
|
+ } else if (allowedAttr === '') {
|
|
|
+ return true
|
|
|
+ } else {
|
|
|
+ const list = allowedAttr.split(',')
|
|
|
+ // text is a subset of children
|
|
|
+ if (allowedType === MismatchTypes.TEXT && list.includes('children')) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ return allowedAttr.split(',').includes(MismatchTypeString[allowedType])
|
|
|
+ }
|
|
|
+}
|