fix(ssr): fix hydration error on falsy v-if inside transition/keep-alive

fix #5352
This commit is contained in:
Evan You 2022-05-18 09:28:18 +08:00
parent c65b805ef1
commit ee4186ef9e
11 changed files with 119 additions and 53 deletions

View File

@ -280,46 +280,92 @@ describe('ssr: components', () => {
`) `)
}) })
test('built-in fallthroughs', () => { describe('built-in fallthroughs', () => {
expect(compile(`<transition><div/></transition>`).code) test('transition', () => {
.toMatchInlineSnapshot(` expect(compile(`<transition><div/></transition>`).code)
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") .toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
}" }"
`) `)
})
// should inject attrs if root with coomments test('keep-alive', () => {
expect(compile(`<!--root--><transition><div/></transition>`).code) expect(compile(`<keep-alive><foo/></keep-alive>`).code)
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\") "const { resolveComponent: _resolveComponent } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`) const _component_foo = _resolveComponent(\\"foo\\")
}"
`)
// should not inject attrs if not root _push(_ssrRenderComponent(_component_foo, _attrs, null, _parent))
expect(compile(`<div/><transition><div/></transition>`).code) }"
.toMatchInlineSnapshot(` `)
" })
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[--><div></div><div></div><!--]-->\`)
}"
`)
expect(compile(`<keep-alive><foo/></keep-alive>`).code) test('should inject attrs if root with coomments', () => {
.toMatchInlineSnapshot(` expect(compile(`<!--root--><transition><div/></transition>`).code)
"const { resolveComponent: _resolveComponent } = require(\\"vue\\") .toMatchInlineSnapshot(`
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\") "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
const _component_foo = _resolveComponent(\\"foo\\") _push(\`<!--[--><!--root--><div\${_ssrRenderAttrs(_attrs)}></div><!--]-->\`)
}"
`)
})
_push(_ssrRenderComponent(_component_foo, _attrs, null, _parent)) test('should not inject attrs if not root', () => {
}" expect(compile(`<div/><transition><div/></transition>`).code)
`) .toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<!--[--><div></div><div></div><!--]-->\`)
}"
`)
})
// #5352
test('should push marker string if is slot root', () => {
expect(
compile(`<foo><transition><div v-if="false"/></transition></foo>`)
.code
).toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode, Transition: _Transition, createVNode: _createVNode } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent, _attrs) {
const _component_foo = _resolveComponent(\\"foo\\")
_push(_ssrRenderComponent(_component_foo, _attrs, {
default: _withCtx((_, _push, _parent, _scopeId) => {
if (_push) {
_push(\`\`)
if (false) {
_push(\`<div\${_scopeId}></div>\`)
} else {
_push(\`<!---->\`)
}
} else {
return [
_createVNode(_Transition, null, {
default: _withCtx(() => [
false
? (_openBlock(), _createBlock(\\"div\\", { key: 0 }))
: _createCommentVNode(\\"v-if\\", true)
]),
_: 1 /* STABLE */
})
]
}
}),
_: 1 /* STABLE */
}, _parent))
}"
`)
})
}) })
// transition-group should flatten and concat its children fragments into // transition-group should flatten and concat its children fragments into

View File

@ -51,7 +51,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const isFragment = const isFragment =
ast.children.length > 1 && ast.children.some(c => !isText(c)) ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment) processChildren(ast, context, isFragment)
ast.codegenNode = createBlockStatement(context.body) ast.codegenNode = createBlockStatement(context.body)
// Finalize helpers. // Finalize helpers.
@ -125,8 +125,12 @@ function createChildContext(
) )
} }
interface Container {
children: TemplateChildNode[]
}
export function processChildren( export function processChildren(
children: TemplateChildNode[], parent: Container,
context: SSRTransformContext, context: SSRTransformContext,
asFragment = false, asFragment = false,
disableNestedFragments = false disableNestedFragments = false
@ -134,6 +138,7 @@ export function processChildren(
if (asFragment) { if (asFragment) {
context.pushStringPart(`<!--[-->`) context.pushStringPart(`<!--[-->`)
} }
const { children } = parent
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
switch (child.type) { switch (child.type) {
@ -143,7 +148,7 @@ export function processChildren(
ssrProcessElement(child, context) ssrProcessElement(child, context)
break break
case ElementTypes.COMPONENT: case ElementTypes.COMPONENT:
ssrProcessComponent(child, context) ssrProcessComponent(child, context, parent)
break break
case ElementTypes.SLOT: case ElementTypes.SLOT:
ssrProcessSlotOutlet(child, context) ssrProcessSlotOutlet(child, context)
@ -208,12 +213,12 @@ export function processChildren(
} }
export function processChildrenAsStatement( export function processChildrenAsStatement(
children: TemplateChildNode[], parent: Container,
parentContext: SSRTransformContext, parentContext: SSRTransformContext,
asFragment = false, asFragment = false,
withSlotScopeId = parentContext.withSlotScopeId withSlotScopeId = parentContext.withSlotScopeId
): BlockStatement { ): BlockStatement {
const childContext = createChildContext(parentContext, withSlotScopeId) const childContext = createChildContext(parentContext, withSlotScopeId)
processChildren(children, childContext, asFragment) processChildren(parent, childContext, asFragment)
return createBlockStatement(childContext.body) return createBlockStatement(childContext.body)
} }

View File

@ -58,7 +58,10 @@ import { buildSSRProps } from './ssrTransformElement'
// pass and complete them in the 2nd pass. // pass and complete them in the 2nd pass.
const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>() const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
const WIP_SLOT = Symbol()
interface WIPSlotEntry { interface WIPSlotEntry {
type: typeof WIP_SLOT
fn: FunctionExpression fn: FunctionExpression
children: TemplateChildNode[] children: TemplateChildNode[]
vnodeBranch: ReturnStatement vnodeBranch: ReturnStatement
@ -143,6 +146,7 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
loc loc
) )
wipEntries.push({ wipEntries.push({
type: WIP_SLOT,
fn, fn,
children, children,
// also collect the corresponding vnode branch built earlier // also collect the corresponding vnode branch built earlier
@ -182,7 +186,8 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
export function ssrProcessComponent( export function ssrProcessComponent(
node: ComponentNode, node: ComponentNode,
context: SSRTransformContext context: SSRTransformContext,
parent: { children: TemplateChildNode[] }
) { ) {
const component = componentTypeMap.get(node)! const component = componentTypeMap.get(node)!
if (!node.ssrCodegenNode) { if (!node.ssrCodegenNode) {
@ -196,13 +201,19 @@ export function ssrProcessComponent(
} else { } else {
// real fall-through: Transition / KeepAlive // real fall-through: Transition / KeepAlive
// just render its children. // just render its children.
processChildren(node.children, context) // #5352: if is at root level of a slot, push an empty string.
// this does not affect the final output, but avoids all-comment slot
// content of being treated as empty by ssrRenderSlot().
if ((parent as WIPSlotEntry).type === WIP_SLOT) {
context.pushStringPart(``)
}
processChildren(node, context)
} }
} else { } else {
// finish up slot function expressions from the 1st pass. // finish up slot function expressions from the 1st pass.
const wipEntries = wipMap.get(node) || [] const wipEntries = wipMap.get(node) || []
for (let i = 0; i < wipEntries.length; i++) { for (let i = 0; i < wipEntries.length; i++) {
const { fn, children, vnodeBranch } = wipEntries[i] const { fn, vnodeBranch } = wipEntries[i]
// For each slot, we generate two branches: one SSR-optimized branch and // For each slot, we generate two branches: one SSR-optimized branch and
// one normal vnode-based branch. The branches are taken based on the // one normal vnode-based branch. The branches are taken based on the
// presence of the 2nd `_push` argument (which is only present if the slot // presence of the 2nd `_push` argument (which is only present if the slot
@ -210,7 +221,7 @@ export function ssrProcessComponent(
fn.body = createIfStatement( fn.body = createIfStatement(
createSimpleExpression(`_push`, false), createSimpleExpression(`_push`, false),
processChildrenAsStatement( processChildrenAsStatement(
children, wipEntries[i],
context, context,
false, false,
true /* withSlotScopeId */ true /* withSlotScopeId */

View File

@ -428,7 +428,7 @@ export function ssrProcessElement(
if (rawChildren) { if (rawChildren) {
context.pushStringPart(rawChildren) context.pushStringPart(rawChildren)
} else if (node.children.length) { } else if (node.children.length) {
processChildren(node.children, context) processChildren(node, context)
} }
if (!isVoidTag(node.tag)) { if (!isVoidTag(node.tag)) {

View File

@ -65,7 +65,7 @@ export function ssrProcessSlotOutlet(
// has fallback content // has fallback content
if (node.children.length) { if (node.children.length) {
const fallbackRenderFn = createFunctionExpression([]) const fallbackRenderFn = createFunctionExpression([])
fallbackRenderFn.body = processChildrenAsStatement(node.children, context) fallbackRenderFn.body = processChildrenAsStatement(node, context)
// _renderSlot(slots, name, props, fallback, ...) // _renderSlot(slots, name, props, fallback, ...)
renderCall.arguments[3] = fallbackRenderFn renderCall.arguments[3] = fallbackRenderFn
} }

View File

@ -66,8 +66,8 @@ export function ssrProcessSuspense(
} }
const { slotsExp, wipSlots } = wipEntry const { slotsExp, wipSlots } = wipEntry
for (let i = 0; i < wipSlots.length; i++) { for (let i = 0; i < wipSlots.length; i++) {
const { fn, children } = wipSlots[i] const slot = wipSlots[i]
fn.body = processChildrenAsStatement(children, context) slot.fn.body = processChildrenAsStatement(slot, context)
} }
// _push(ssrRenderSuspense(slots)) // _push(ssrRenderSuspense(slots))
context.pushStatement( context.pushStatement(

View File

@ -58,7 +58,7 @@ export function ssrProcessTeleport(
false, // isSlot false, // isSlot
node.loc node.loc
) )
contentRenderFn.body = processChildrenAsStatement(node.children, context) contentRenderFn.body = processChildrenAsStatement(node, context)
context.pushStatement( context.pushStatement(
createCallExpression(context.helper(SSR_RENDER_TELEPORT), [ createCallExpression(context.helper(SSR_RENDER_TELEPORT), [
`_push`, `_push`,

View File

@ -14,7 +14,7 @@ export function ssrProcessTransitionGroup(
context.pushStringPart(`>`) context.pushStringPart(`>`)
processChildren( processChildren(
node.children, node,
context, context,
false, false,
/** /**
@ -31,11 +31,11 @@ export function ssrProcessTransitionGroup(
} else { } else {
// static tag // static tag
context.pushStringPart(`<${tag.value!.content}>`) context.pushStringPart(`<${tag.value!.content}>`)
processChildren(node.children, context, false, true) processChildren(node, context, false, true)
context.pushStringPart(`</${tag.value!.content}>`) context.pushStringPart(`</${tag.value!.content}>`)
} }
} else { } else {
// fragment // fragment
processChildren(node.children, context, true, true) processChildren(node, context, true, true)
} }
} }

View File

@ -33,7 +33,7 @@ export function ssrProcessFor(
createForLoopParams(node.parseResult) createForLoopParams(node.parseResult)
) )
renderLoop.body = processChildrenAsStatement( renderLoop.body = processChildrenAsStatement(
node.children, node,
context, context,
needFragmentWrapper needFragmentWrapper
) )

View File

@ -72,5 +72,5 @@ function processIfBranch(
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) && (children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
// optimize away nested fragments when the only child is a ForNode // optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR) !(children.length === 1 && children[0].type === NodeTypes.FOR)
return processChildrenAsStatement(children, context, needFragmentWrapper) return processChildrenAsStatement(branch, context, needFragmentWrapper)
} }

View File

@ -84,5 +84,9 @@ export function ssrRenderSlotInner(
const commentRE = /<!--.*?-->/g const commentRE = /<!--.*?-->/g
function isComment(item: SSRBufferItem) { function isComment(item: SSRBufferItem) {
return typeof item === 'string' && !item.replace(commentRE, '').trim() return (
typeof item === 'string' &&
commentRE.test(item) &&
!item.replace(commentRE, '').trim()
)
} }