Skip to content

Commit e495fa4

Browse files
authored
feat(ssr): render portals (#714)
1 parent aa09f01 commit e495fa4

File tree

3 files changed

+93
-11
lines changed

3 files changed

+93
-11
lines changed

packages/server-renderer/__tests__/renderToString.spec.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import {
55
withScopeId,
66
resolveComponent,
77
ComponentOptions,
8+
Portal,
89
ref,
910
defineComponent
1011
} from 'vue'
1112
import { escapeHtml, mockWarn } from '@vue/shared'
12-
import { renderToString, renderComponent } from '../src/renderToString'
13+
import {
14+
renderToString,
15+
renderComponent,
16+
SSRContext
17+
} from '../src/renderToString'
1318
import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
1419

1520
mockWarn()
@@ -508,6 +513,21 @@ describe('ssr: renderToString', () => {
508513
})
509514
})
510515

516+
test('portal', async () => {
517+
const ctx: SSRContext = {}
518+
await renderToString(
519+
h(
520+
Portal,
521+
{
522+
target: `#target`
523+
},
524+
h('span', 'hello')
525+
),
526+
ctx
527+
)
528+
expect(ctx.portals!['#target']).toBe('<span>hello</span>')
529+
})
530+
511531
describe('scopeId', () => {
512532
// note: here we are only testing scopeId handling for vdom serialization.
513533
// compiled srr render functions will include scopeId directly in strings.

packages/server-renderer/src/helpers/ssrRenderSlot.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function ssrRenderSlot(
1616
slotProps: Props,
1717
fallbackRenderFn: (() => void) | null,
1818
push: PushFn,
19-
parentComponent: ComponentInternalInstance | null = null
19+
parentComponent: ComponentInternalInstance
2020
) {
2121
const slotFn = slots[slotName]
2222
// template-compiled slots are always rendered as fragments

packages/server-renderer/src/renderToString.ts

+71-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
Portal,
1212
ssrUtils,
1313
Slots,
14-
warn
14+
warn,
15+
createApp
1516
} from 'vue'
1617
import {
1718
ShapeFlags,
@@ -47,9 +48,22 @@ const {
4748
type SSRBuffer = SSRBufferItem[]
4849
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
4950
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
51+
5052
export type PushFn = (item: SSRBufferItem) => void
53+
5154
export type Props = Record<string, unknown>
5255

56+
const ssrContextKey = Symbol()
57+
58+
export type SSRContext = {
59+
[key: string]: any
60+
portals?: Record<string, string>
61+
__portalBuffers?: Record<
62+
string,
63+
ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
64+
>
65+
}
66+
5367
function createBuffer() {
5468
let appendable = false
5569
let hasAsync = false
@@ -88,17 +102,33 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
88102
return ret
89103
}
90104

91-
export async function renderToString(input: App | VNode): Promise<string> {
105+
export async function renderToString(
106+
input: App | VNode,
107+
context: SSRContext = {}
108+
): Promise<string> {
92109
let buffer: ResolvedSSRBuffer
93110
if (isVNode(input)) {
94-
// raw vnode, wrap with component
95-
buffer = await renderComponent({ render: () => input })
111+
// raw vnode, wrap with app (for context)
112+
return renderToString(createApp({ render: () => input }), context)
96113
} else {
97114
// rendering an app
98115
const vnode = createVNode(input._component, input._props)
99116
vnode.appContext = input._context
117+
// provide the ssr context to the tree
118+
input.provide(ssrContextKey, context)
100119
buffer = await renderComponentVNode(vnode)
101120
}
121+
122+
// resolve portals
123+
if (context.__portalBuffers) {
124+
context.portals = context.portals || {}
125+
for (const key in context.__portalBuffers) {
126+
// note: it's OK to await sequentially here because the Promises were
127+
// created eagerly in parallel.
128+
context.portals[key] = unrollBuffer(await context.__portalBuffers[key])
129+
}
130+
}
131+
102132
return unrollBuffer(buffer)
103133
}
104134

@@ -132,7 +162,7 @@ function renderComponentVNode(
132162
}
133163

134164
type SSRRenderFunction = (
135-
ctx: any,
165+
context: any,
136166
push: (item: any) => void,
137167
parentInstance: ComponentInternalInstance
138168
) => void
@@ -206,7 +236,7 @@ function renderComponentSubTree(
206236
function renderVNode(
207237
push: PushFn,
208238
vnode: VNode,
209-
parentComponent: ComponentInternalInstance | null = null
239+
parentComponent: ComponentInternalInstance
210240
) {
211241
const { type, shapeFlag, children } = vnode
212242
switch (type) {
@@ -222,7 +252,7 @@ function renderVNode(
222252
push(`<!---->`)
223253
break
224254
case Portal:
225-
// TODO
255+
renderPortal(vnode, parentComponent)
226256
break
227257
default:
228258
if (shapeFlag & ShapeFlags.ELEMENT) {
@@ -244,7 +274,7 @@ function renderVNode(
244274
export function renderVNodeChildren(
245275
push: PushFn,
246276
children: VNodeArrayChildren,
247-
parentComponent: ComponentInternalInstance | null = null
277+
parentComponent: ComponentInternalInstance
248278
) {
249279
for (let i = 0; i < children.length; i++) {
250280
renderVNode(push, normalizeVNode(children[i]), parentComponent)
@@ -254,7 +284,7 @@ export function renderVNodeChildren(
254284
function renderElement(
255285
push: PushFn,
256286
vnode: VNode,
257-
parentComponent: ComponentInternalInstance | null = null
287+
parentComponent: ComponentInternalInstance
258288
) {
259289
const tag = vnode.type as string
260290
const { props, children, shapeFlag, scopeId } = vnode
@@ -305,3 +335,35 @@ function renderElement(
305335
push(`</${tag}>`)
306336
}
307337
}
338+
339+
function renderPortal(
340+
vnode: VNode,
341+
parentComponent: ComponentInternalInstance
342+
) {
343+
const target = vnode.props && vnode.props.target
344+
if (!target) {
345+
console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
346+
return []
347+
}
348+
if (!isString(target)) {
349+
console.warn(
350+
`[@vue/server-renderer] Portal target must be a query selector string.`
351+
)
352+
return []
353+
}
354+
355+
const { buffer, push, hasAsync } = createBuffer()
356+
renderVNodeChildren(
357+
push,
358+
vnode.children as VNodeArrayChildren,
359+
parentComponent
360+
)
361+
const context = parentComponent.appContext.provides[
362+
ssrContextKey as any
363+
] as SSRContext
364+
const portalBuffers =
365+
context.__portalBuffers || (context.__portalBuffers = {})
366+
portalBuffers[target] = hasAsync()
367+
? Promise.all(buffer)
368+
: (buffer as ResolvedSSRBuffer)
369+
}

0 commit comments

Comments
 (0)