Skip to content

Commit a3cc970

Browse files
committed
feat(ssr/suspense): suspense hydration
In order to support hydration of async components, server-rendered fragments must be explicitly marked with comment nodes.
1 parent b3d7d64 commit a3cc970

19 files changed

+385
-139
lines changed

packages/compiler-core/__tests__/hydration.spec.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe('SSR hydration', () => {
9898
const msg = ref('foo')
9999
const fn = jest.fn()
100100
const { vnode, container } = mountWithHydration(
101-
'<div><span>foo</span><span class="foo"></span></div>',
101+
'<div><!----><span>foo</span><!----><span class="foo"></span><!----><!----></div>',
102102
() =>
103103
h('div', [
104104
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
@@ -136,7 +136,9 @@ describe('SSR hydration', () => {
136136

137137
msg.value = 'bar'
138138
await nextTick()
139-
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
139+
expect(vnode.el.innerHTML).toBe(
140+
`<!----><span>bar</span><!----><span class="bar"></span><!----><!---->`
141+
)
140142
})
141143

142144
test('portal', async () => {

packages/compiler-ssr/__tests__/ssrComponent.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,11 @@ describe('ssr: components', () => {
219219
foo: ({ list }, _push, _parent, _scopeId) => {
220220
if (_push) {
221221
if (_ctx.ok) {
222-
_push(\`<div\${_scopeId}>\`)
222+
_push(\`<div\${_scopeId}><!--1-->\`)
223223
_ssrRenderList(list, (i) => {
224224
_push(\`<span\${_scopeId}></span>\`)
225225
})
226-
_push(\`</div>\`)
226+
_push(\`<!--0--></div>\`)
227227
} else {
228228
_push(\`<!---->\`)
229229
}
@@ -242,11 +242,11 @@ describe('ssr: components', () => {
242242
bar: ({ ok }, _push, _parent, _scopeId) => {
243243
if (_push) {
244244
if (ok) {
245-
_push(\`<div\${_scopeId}>\`)
245+
_push(\`<div\${_scopeId}><!--1-->\`)
246246
_ssrRenderList(_ctx.list, (i) => {
247247
_push(\`<span\${_scopeId}></span>\`)
248248
})
249-
_push(\`</div>\`)
249+
_push(\`<!--0--></div>\`)
250250
} else {
251251
_push(\`<!---->\`)
252252
}
@@ -281,7 +281,7 @@ describe('ssr: components', () => {
281281
.toMatchInlineSnapshot(`
282282
"
283283
return function ssrRender(_ctx, _push, _parent) {
284-
_push(\`<div></div>\`)
284+
_push(\`<!--1--><div></div><!--0-->\`)
285285
}"
286286
`)
287287

packages/compiler-ssr/__tests__/ssrVFor.spec.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ describe('ssr: v-for', () => {
66
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
77
88
return function ssrRender(_ctx, _push, _parent) {
9+
_push(\`<!--1-->\`)
910
_ssrRenderList(_ctx.list, (i) => {
1011
_push(\`<div></div>\`)
1112
})
13+
_push(\`<!--0-->\`)
1214
}"
1315
`)
1416
})
@@ -19,9 +21,11 @@ describe('ssr: v-for', () => {
1921
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
2022
2123
return function ssrRender(_ctx, _push, _parent) {
24+
_push(\`<!--1-->\`)
2225
_ssrRenderList(_ctx.list, (i) => {
2326
_push(\`<div>foo<span>bar</span></div>\`)
2427
})
28+
_push(\`<!--0-->\`)
2529
}"
2630
`)
2731
})
@@ -37,17 +41,19 @@ describe('ssr: v-for', () => {
3741
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
3842
3943
return function ssrRender(_ctx, _push, _parent) {
44+
_push(\`<!--1-->\`)
4045
_ssrRenderList(_ctx.list, (row, i) => {
41-
_push(\`<div>\`)
46+
_push(\`<div><!--1-->\`)
4247
_ssrRenderList(row, (j) => {
4348
_push(\`<div>\${
4449
_ssrInterpolate(i)
4550
},\${
4651
_ssrInterpolate(j)
4752
}</div>\`)
4853
})
49-
_push(\`</div>\`)
54+
_push(\`<!--0--></div>\`)
5055
})
56+
_push(\`<!--0-->\`)
5157
}"
5258
`)
5359
})
@@ -58,9 +64,11 @@ describe('ssr: v-for', () => {
5864
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
5965
6066
return function ssrRender(_ctx, _push, _parent) {
67+
_push(\`<!--1-->\`)
6168
_ssrRenderList(_ctx.list, (i) => {
62-
_push(\`\${_ssrInterpolate(i)}\`)
69+
_push(\`<!--1-->\${_ssrInterpolate(i)}<!--0-->\`)
6370
})
71+
_push(\`<!--0-->\`)
6472
}"
6573
`)
6674
})
@@ -73,9 +81,11 @@ describe('ssr: v-for', () => {
7381
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
7482
7583
return function ssrRender(_ctx, _push, _parent) {
84+
_push(\`<!--1-->\`)
7685
_ssrRenderList(_ctx.list, (i) => {
7786
_push(\`<span>\${_ssrInterpolate(i)}</span>\`)
7887
})
88+
_push(\`<!--0-->\`)
7989
}"
8090
`)
8191
})
@@ -89,13 +99,15 @@ describe('ssr: v-for', () => {
8999
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
90100
91101
return function ssrRender(_ctx, _push, _parent) {
102+
_push(\`<!--1-->\`)
92103
_ssrRenderList(_ctx.list, (i) => {
93-
_push(\`<span>\${
104+
_push(\`<!--1--><span>\${
94105
_ssrInterpolate(i)
95106
}</span><span>\${
96107
_ssrInterpolate(i + 1)
97-
}</span>\`)
108+
}</span><!--0-->\`)
98109
})
110+
_push(\`<!--0-->\`)
99111
}"
100112
`)
101113
})
@@ -111,9 +123,11 @@ describe('ssr: v-for', () => {
111123
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
112124
113125
return function ssrRender(_ctx, _push, _parent) {
126+
_push(\`<!--1-->\`)
114127
_ssrRenderList(_ctx.list, ({ foo }, index) => {
115128
_push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
116129
})
130+
_push(\`<!--0-->\`)
117131
}"
118132
`)
119133
})

packages/compiler-ssr/__tests__/ssrVIf.spec.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('ssr: v-if', () => {
8080
"
8181
return function ssrRender(_ctx, _push, _parent) {
8282
if (_ctx.foo) {
83-
_push(\`hello\`)
83+
_push(\`<!--1-->hello<!--0-->\`)
8484
} else {
8585
_push(\`<!---->\`)
8686
}
@@ -110,7 +110,7 @@ describe('ssr: v-if', () => {
110110
"
111111
return function ssrRender(_ctx, _push, _parent) {
112112
if (_ctx.foo) {
113-
_push(\`<div>hi</div><div>ho</div>\`)
113+
_push(\`<!--1--><div>hi</div><div>ho</div><!--0-->\`)
114114
} else {
115115
_push(\`<!---->\`)
116116
}
@@ -126,9 +126,11 @@ describe('ssr: v-if', () => {
126126
127127
return function ssrRender(_ctx, _push, _parent) {
128128
if (_ctx.foo) {
129+
_push(\`<!--1-->\`)
129130
_ssrRenderList(_ctx.list, (i) => {
130131
_push(\`<div></div>\`)
131132
})
133+
_push(\`<!--0-->\`)
132134
} else {
133135
_push(\`<!---->\`)
134136
}
@@ -145,7 +147,7 @@ describe('ssr: v-if', () => {
145147
"
146148
return function ssrRender(_ctx, _push, _parent) {
147149
if (_ctx.foo) {
148-
_push(\`<div>hi</div><div>ho</div>\`)
150+
_push(\`<!--1--><div>hi</div><div>ho</div><!--0-->\`)
149151
} else {
150152
_push(\`<div></div>\`)
151153
}

packages/compiler-ssr/src/ssrCodegenTransform.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
createBlockStatement,
1111
CompilerOptions,
1212
IfStatement,
13-
CallExpression
13+
CallExpression,
14+
isText
1415
} from '@vue/compiler-dom'
1516
import { isString, escapeHtml } from '@vue/shared'
1617
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
@@ -28,7 +29,9 @@ import { ssrProcessElement } from './transforms/ssrTransformElement'
2829

2930
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
3031
const context = createSSRTransformContext(ast, options)
31-
processChildren(ast.children, context)
32+
const isFragment =
33+
ast.children.length > 1 && ast.children.some(c => !isText(c))
34+
processChildren(ast.children, context, isFragment)
3235
ast.codegenNode = createBlockStatement(context.body)
3336

3437
// Finalize helpers.
@@ -104,8 +107,12 @@ function createChildContext(
104107

105108
export function processChildren(
106109
children: TemplateChildNode[],
107-
context: SSRTransformContext
110+
context: SSRTransformContext,
111+
asFragment = false
108112
) {
113+
if (asFragment) {
114+
context.pushStringPart(`<!--1-->`)
115+
}
109116
for (let i = 0; i < children.length; i++) {
110117
const child = children[i]
111118
if (child.type === NodeTypes.ELEMENT) {
@@ -128,14 +135,18 @@ export function processChildren(
128135
ssrProcessFor(child, context)
129136
}
130137
}
138+
if (asFragment) {
139+
context.pushStringPart(`<!--0-->`)
140+
}
131141
}
132142

133143
export function processChildrenAsStatement(
134144
children: TemplateChildNode[],
135145
parentContext: SSRTransformContext,
146+
asFragment = false,
136147
withSlotScopeId = parentContext.withSlotScopeId
137148
): BlockStatement {
138149
const childContext = createChildContext(parentContext, withSlotScopeId)
139-
processChildren(children, childContext)
150+
processChildren(children, childContext, asFragment)
140151
return createBlockStatement(childContext.body)
141152
}

packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
traverseNode,
3131
ExpressionNode,
3232
TemplateNode,
33-
SUSPENSE
33+
SUSPENSE,
34+
TRANSITION_GROUP
3435
} from '@vue/compiler-dom'
3536
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
3637
import {
@@ -151,7 +152,7 @@ export function ssrProcessComponent(
151152
return ssrProcessSuspense(node, context)
152153
} else {
153154
// real fall-through (e.g. KeepAlive): just render its children.
154-
processChildren(node.children, context)
155+
processChildren(node.children, context, component === TRANSITION_GROUP)
155156
}
156157
} else {
157158
// finish up slot function expressions from the 1st pass.
@@ -167,6 +168,7 @@ export function ssrProcessComponent(
167168
processChildrenAsStatement(
168169
children,
169170
context,
171+
false,
170172
true /* withSlotScopeId */
171173
),
172174
vnodeBranch

packages/compiler-ssr/src/transforms/ssrVFor.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
processFor,
55
createCallExpression,
66
createFunctionExpression,
7-
createForLoopParams
7+
createForLoopParams,
8+
NodeTypes
89
} from '@vue/compiler-dom'
910
import {
1011
SSRTransformContext,
@@ -21,14 +22,23 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
2122
// This is called during the 2nd transform pass to construct the SSR-sepcific
2223
// codegen nodes.
2324
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
25+
const needFragmentWrapper =
26+
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
2427
const renderLoop = createFunctionExpression(
2528
createForLoopParams(node.parseResult)
2629
)
27-
renderLoop.body = processChildrenAsStatement(node.children, context)
30+
renderLoop.body = processChildrenAsStatement(
31+
node.children,
32+
context,
33+
needFragmentWrapper
34+
)
35+
// v-for always renders a fragment
36+
context.pushStringPart(`<!--1-->`)
2837
context.pushStatement(
2938
createCallExpression(context.helper(SSR_RENDER_LIST), [
3039
node.source,
3140
renderLoop
3241
])
3342
)
43+
context.pushStringPart(`<!--0-->`)
3444
}

packages/compiler-ssr/src/transforms/ssrVIf.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import {
44
IfNode,
55
createIfStatement,
66
createBlockStatement,
7-
createCallExpression
7+
createCallExpression,
8+
IfBranchNode,
9+
BlockStatement,
10+
NodeTypes
811
} from '@vue/compiler-dom'
912
import {
1013
SSRTransformContext,
@@ -23,17 +26,14 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
2326
const [rootBranch] = node.branches
2427
const ifStatement = createIfStatement(
2528
rootBranch.condition!,
26-
processChildrenAsStatement(rootBranch.children, context)
29+
processIfBranch(rootBranch, context)
2730
)
2831
context.pushStatement(ifStatement)
2932

3033
let currentIf = ifStatement
3134
for (let i = 1; i < node.branches.length; i++) {
3235
const branch = node.branches[i]
33-
const branchBlockStatement = processChildrenAsStatement(
34-
branch.children,
35-
context
36-
)
36+
const branchBlockStatement = processIfBranch(branch, context)
3737
if (branch.condition) {
3838
// else-if
3939
currentIf = currentIf.alternate = createIfStatement(
@@ -52,3 +52,15 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
5252
])
5353
}
5454
}
55+
56+
function processIfBranch(
57+
branch: IfBranchNode,
58+
context: SSRTransformContext
59+
): BlockStatement {
60+
const { children } = branch
61+
const needFragmentWrapper =
62+
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
63+
// optimize away nested fragments when the only child is a ForNode
64+
!(children.length === 1 && children[0].type === NodeTypes.FOR)
65+
return processChildrenAsStatement(children, context, needFragmentWrapper)
66+
}

packages/runtime-core/src/component.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ export interface ComponentInternalInstance {
144144

145145
// suspense related
146146
asyncDep: Promise<any> | null
147-
asyncResult: unknown
148147
asyncResolved: boolean
149148

150149
// storage for any extra properties
@@ -215,7 +214,6 @@ export function createComponentInstance(
215214

216215
// async dependency management
217216
asyncDep: null,
218-
asyncResult: null,
219217
asyncResolved: false,
220218

221219
// user namespace for storing whatever the user assigns to `this`
@@ -367,7 +365,7 @@ function setupStatefulComponent(
367365
if (isPromise(setupResult)) {
368366
if (isSSR) {
369367
// return the promise so server-renderer can wait on it
370-
return setupResult.then(resolvedResult => {
368+
return setupResult.then((resolvedResult: unknown) => {
371369
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
372370
})
373371
} else if (__FEATURE_SUSPENSE__) {

0 commit comments

Comments
 (0)