Skip to content

Commit 41db49d

Browse files
committed
fix(ssr): support dynamic components that resolve to element or vnode
fix #1508
1 parent d7184c9 commit 41db49d

File tree

6 files changed

+117
-18
lines changed

6 files changed

+117
-18
lines changed

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,21 @@ describe('ssr: components', () => {
2020
test('dynamic component', () => {
2121
expect(compile(`<component is="foo" prop="b" />`).code)
2222
.toMatchInlineSnapshot(`
23-
"const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps } = require(\\"vue\\")
24-
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
23+
"const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require(\\"vue\\")
24+
const { ssrRenderVNode: _ssrRenderVNode } = require(\\"@vue/server-renderer\\")
2525
2626
return function ssrRender(_ctx, _push, _parent, _attrs) {
27-
_push(_ssrRenderComponent(_resolveDynamicComponent(\\"foo\\"), _mergeProps({ prop: \\"b\\" }, _attrs), null, _parent))
27+
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(\\"foo\\"), _mergeProps({ prop: \\"b\\" }, _attrs), null), _parent)
2828
}"
2929
`)
3030

3131
expect(compile(`<component :is="foo" prop="b" />`).code)
3232
.toMatchInlineSnapshot(`
33-
"const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps } = require(\\"vue\\")
34-
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
33+
"const { resolveDynamicComponent: _resolveDynamicComponent, mergeProps: _mergeProps, createVNode: _createVNode } = require(\\"vue\\")
34+
const { ssrRenderVNode: _ssrRenderVNode } = require(\\"@vue/server-renderer\\")
3535
3636
return function ssrRender(_ctx, _push, _parent, _attrs) {
37-
_push(_ssrRenderComponent(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: \\"b\\" }, _attrs), null, _parent))
37+
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: \\"b\\" }, _attrs), null), _parent)
3838
}"
3939
`)
4040
})

packages/compiler-ssr/src/runtimeHelpers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { registerRuntimeHelpers } from '@vue/compiler-dom'
22

33
export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`)
4+
export const SSR_RENDER_VNODE = Symbol(`ssrRenderVNode`)
45
export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`)
56
export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`)
67
export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`)
@@ -18,6 +19,7 @@ export const SSR_RENDER_SUSPENSE = Symbol(`ssrRenderSuspense`)
1819

1920
export const ssrHelpers = {
2021
[SSR_INTERPOLATE]: `ssrInterpolate`,
22+
[SSR_RENDER_VNODE]: `ssrRenderVNode`,
2123
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
2224
[SSR_RENDER_SLOT]: `ssrRenderSlot`,
2325
[SSR_RENDER_CLASS]: `ssrRenderClass`,

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

+44-11
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
buildSlots,
1212
FunctionExpression,
1313
TemplateChildNode,
14-
TELEPORT,
1514
createIfStatement,
1615
createSimpleExpression,
1716
getBaseTransformPreset,
@@ -31,9 +30,12 @@ import {
3130
ExpressionNode,
3231
TemplateNode,
3332
SUSPENSE,
34-
TRANSITION_GROUP
33+
TELEPORT,
34+
TRANSITION_GROUP,
35+
CREATE_VNODE,
36+
CallExpression
3537
} from '@vue/compiler-dom'
36-
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
38+
import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
3739
import {
3840
SSRTransformContext,
3941
processChildren,
@@ -58,7 +60,10 @@ interface WIPSlotEntry {
5860
vnodeBranch: ReturnStatement
5961
}
6062

61-
const componentTypeMap = new WeakMap<ComponentNode, symbol>()
63+
const componentTypeMap = new WeakMap<
64+
ComponentNode,
65+
string | symbol | CallExpression
66+
>()
6267

6368
// ssr component transform is done in two phases:
6469
// In phase 1. we use `buildSlot` to analyze the children of the component into
@@ -75,8 +80,9 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
7580
}
7681

7782
const component = resolveComponentType(node, context, true /* ssr */)
83+
componentTypeMap.set(node, component)
84+
7885
if (isSymbol(component)) {
79-
componentTypeMap.set(node, component)
8086
if (component === SUSPENSE) {
8187
return ssrTransformSuspense(node, context)
8288
}
@@ -134,20 +140,38 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
134140
? buildSlots(node, context, buildSSRSlotFn).slots
135141
: `null`
136142

137-
node.ssrCodegenNode = createCallExpression(
138-
context.helper(SSR_RENDER_COMPONENT),
139-
[component, props, slots, `_parent`]
140-
)
143+
if (typeof component !== 'string') {
144+
// dynamic component that resolved to a `resolveDynamicComponent` call
145+
// expression - since the reoslved result may be a plain element (string)
146+
// or a VNode, handle it with `renderVNode`.
147+
node.ssrCodegenNode = createCallExpression(
148+
context.helper(SSR_RENDER_VNODE),
149+
[
150+
`_push`,
151+
createCallExpression(context.helper(CREATE_VNODE), [
152+
component,
153+
props,
154+
slots
155+
]),
156+
`_parent`
157+
]
158+
)
159+
} else {
160+
node.ssrCodegenNode = createCallExpression(
161+
context.helper(SSR_RENDER_COMPONENT),
162+
[component, props, slots, `_parent`]
163+
)
164+
}
141165
}
142166
}
143167

144168
export function ssrProcessComponent(
145169
node: ComponentNode,
146170
context: SSRTransformContext
147171
) {
172+
const component = componentTypeMap.get(node)!
148173
if (!node.ssrCodegenNode) {
149174
// this is a built-in component that fell-through.
150-
const component = componentTypeMap.get(node)!
151175
if (component === TELEPORT) {
152176
return ssrProcessTeleport(node, context)
153177
} else if (component === SUSPENSE) {
@@ -176,7 +200,16 @@ export function ssrProcessComponent(
176200
vnodeBranch
177201
)
178202
}
179-
context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode]))
203+
if (typeof component === 'string') {
204+
// static component
205+
context.pushStatement(
206+
createCallExpression(`_push`, [node.ssrCodegenNode])
207+
)
208+
} else {
209+
// dynamic component (`resolveDynamicComponent` call)
210+
// the codegen node is a `renderVNode` call
211+
context.pushStatement(node.ssrCodegenNode)
212+
}
180213
}
181214
}
182215

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { createApp, createVNode } from 'vue'
2+
import { renderToString } from '../src/renderToString'
3+
4+
describe('ssr: dynamic component', () => {
5+
test('resolved to component', async () => {
6+
expect(
7+
await renderToString(
8+
createApp({
9+
components: {
10+
one: {
11+
template: `<div><slot/></div>`
12+
}
13+
},
14+
template: `<component :is="'one'"><span>slot</span></component>`
15+
})
16+
)
17+
).toBe(`<div><!--[--><span>slot</span><!--]--></div>`)
18+
})
19+
20+
test('resolve to element', async () => {
21+
expect(
22+
await renderToString(
23+
createApp({
24+
template: `<component :is="'p'"><span>slot</span></component>`
25+
})
26+
)
27+
).toBe(`<p><span>slot</span></p>`)
28+
})
29+
30+
test('resolve to component vnode', async () => {
31+
const Child = {
32+
props: ['id'],
33+
template: `<div>{{ id }}<slot/></div>`
34+
}
35+
expect(
36+
await renderToString(
37+
createApp({
38+
setup() {
39+
return {
40+
vnode: createVNode(Child, { id: 'test' })
41+
}
42+
},
43+
template: `<component :is="vnode"><span>slot</span></component>`
44+
})
45+
)
46+
).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`)
47+
})
48+
49+
test('resolve to element vnode', async () => {
50+
expect(
51+
await renderToString(
52+
createApp({
53+
setup() {
54+
return {
55+
vnode: createVNode('div', { id: 'test' })
56+
}
57+
},
58+
template: `<component :is="vnode"><span>slot</span></component>`
59+
})
60+
)
61+
).toBe(`<div id="test"><span>slot</span></div>`)
62+
})
63+
})

packages/server-renderer/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { renderToString } from './renderToString'
44
export { renderToStream } from './renderToStream'
55

66
// internal runtime helpers
7+
export { renderVNode as ssrRenderVNode } from './render'
78
export { ssrRenderComponent } from './helpers/ssrRenderComponent'
89
export { ssrRenderSlot } from './helpers/ssrRenderSlot'
910
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'

packages/server-renderer/src/render.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function renderComponentSubTree(
142142
return getBuffer()
143143
}
144144

145-
function renderVNode(
145+
export function renderVNode(
146146
push: PushFn,
147147
vnode: VNode,
148148
parentComponent: ComponentInternalInstance

0 commit comments

Comments
 (0)