Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler-core): avoid discrepancies with server-side rendering #6699

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 27 additions & 32 deletions packages/compiler-core/src/transform.ts
Expand Up @@ -15,28 +15,25 @@ import {
CacheExpression,
createCacheExpression,
TemplateLiteral,
createVNodeCall,
ConstantTypes,
ArrayExpression
} from './ast'
import {
isString,
isArray,
NOOP,
PatchFlags,
PatchFlagNames,
EMPTY_OBJ,
capitalize,
camelize
} from '@vue/shared'
import { defaultOnError, defaultOnWarn } from './errors'
import {
TO_DISPLAY_STRING,
FRAGMENT,
helperNameMap,
CREATE_COMMENT
CREATE_COMMENT,
CREATE_STATIC
} from './runtimeHelpers'
import { isVSlot, makeBlock } from './utils'
import { isVSlot, makeBlock, makeFragmentBlock } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
import { CompilerCompatOptions } from './compat/compatConfig'

Expand Down Expand Up @@ -122,6 +119,20 @@ export interface TransformContext
filters?: Set<string>
}

const prefix = '_hoisted_'
const isSingleHoistStaticRoot = (
root: RootNode,
context: TransformContext
): boolean => {
const { children } = root
const { hoists } = context
return (
children.length === 1 &&
(children[0] as any).codegenNode?.content?.startsWith(prefix) &&
(hoists[0] as any)?.callee === CREATE_STATIC
)
}

export function createTransformContext(
root: RootNode,
{
Expand Down Expand Up @@ -282,7 +293,7 @@ export function createTransformContext(
if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
`${prefix}${context.hoists.length}`,
false,
exp.loc,
ConstantTypes.CAN_HOIST
Expand Down Expand Up @@ -338,12 +349,18 @@ export function transform(root: RootNode, options: TransformOptions) {
}

function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
if (children.length === 1) {
const child = children[0]
// #6637
if (isSingleHoistStaticRoot(root, context)) {
// when the root is only one child and it is a static node,
// we need to return a fragment block to keep in line with
// the server-side rendering behavior to prevent warning when hydrating
makeFragmentBlock(root, context)
}
// if the single child is an element, turn it into a block.
if (isSingleElementRoot(root, child) && child.codegenNode) {
else if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
const codegenNode = child.codegenNode
Expand All @@ -359,29 +376,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
}
} else if (children.length > 1) {
// root has multiple nodes - return a fragment block.
let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
__DEV__ &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
undefined,
undefined,
true,
undefined,
false /* isComponent */
)
makeFragmentBlock(root, context)
} else {
// no children = noop. codegen will return null.
}
Expand Down
36 changes: 33 additions & 3 deletions packages/compiler-core/src/utils.ts
Expand Up @@ -23,7 +23,8 @@ import {
VNodeCall,
SimpleExpressionNode,
BlockCodegenNode,
MemoExpression
MemoExpression,
createVNodeCall
} from './ast'
import { TransformContext } from './transform'
import {
Expand All @@ -40,9 +41,10 @@ import {
CREATE_VNODE,
CREATE_ELEMENT_VNODE,
WITH_MEMO,
OPEN_BLOCK
OPEN_BLOCK,
FRAGMENT
} from './runtimeHelpers'
import { isString, isObject, hyphenate, extend, NOOP } from '@vue/shared'
import { isString, isObject, hyphenate, extend, NOOP, PatchFlags, PatchFlagNames } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser'
import { Expression } from '@babel/types'
Expand Down Expand Up @@ -537,3 +539,31 @@ export function makeBlock(
helper(getVNodeBlockHelper(inSSR, node.isComponent))
}
}

export function makeFragmentBlock(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
__DEV__ &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
undefined,
undefined,
true,
undefined,
false
)
}
Expand Up @@ -34,21 +34,25 @@ return function render(_ctx, _cache) {
`;

exports[`stringify static html stringify v-html 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"<pre data-type=\\\\\\"js\\\\\\"><code><span>show-it </span></code></pre><div class><span class>1</span><span class>2</span></div>\\", 2)

return function render(_ctx, _cache) {
return _hoisted_1
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
}"
`;

exports[`stringify static html stringify v-text 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue

const _hoisted_1 = /*#__PURE__*/_createStaticVNode(\\"<pre data-type=\\\\\\"js\\\\\\"><code>&lt;span&gt;show-it &lt;/span&gt;</code></pre><div class><span class>1</span><span class>2</span></div>\\", 2)

return function render(_ctx, _cache) {
return _hoisted_1
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
}"
`;
7 changes: 5 additions & 2 deletions packages/compiler-dom/src/index.ts
Expand Up @@ -17,11 +17,14 @@ import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow'
import { transformTransition } from './transforms/Transition'
import { stringifyStatic } from './transforms/stringifyStatic'
import {
stringifyStatic,
StringifyThresholds
} from './transforms/stringifyStatic'
import { ignoreSideEffectTags } from './transforms/ignoreSideEffectTags'
import { extend } from '@vue/shared'

export { parserOptions }
export { parserOptions, StringifyThresholds }

export const DOMNodeTransforms: NodeTransform[] = [
transformStyle,
Expand Down
21 changes: 21 additions & 0 deletions packages/runtime-core/__tests__/hydration.spec.ts
Expand Up @@ -17,6 +17,7 @@ import {
renderSlot
} from '@vue/runtime-dom'
import { renderToString, SSRContext } from '@vue/server-renderer'
import { StringifyThresholds } from '@vue/compiler-dom'
import { PatchFlags } from '../../shared/src'

function mountWithHydration(html: string, render: () => any) {
Expand Down Expand Up @@ -545,6 +546,26 @@ describe('SSR hydration', () => {
expect(text.textContent).toBe('bye')
})

// #6637
test('the result of client render should be the same as server render', async () => {
const App = {
// the quantity must reach the threshold to be reproduce
template: `<div></div>`.repeat(StringifyThresholds.NODE_COUNT)
}
const container = document.createElement('div')

// server render
const serverSide = await renderToString(h(App))
container.innerHTML = serverSide

// client render
createSSRApp(App).mount(container)
const clientSide = container.innerHTML

expect(`Hydration node mismatch`).not.toHaveBeenWarned()
expect(serverSide).toBe(clientSide)
})

test('handle click error in ssr mode', async () => {
const App = {
setup() {
Expand Down