diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts
index ed60dcaeb8d..2ac929ac1c4 100644
--- a/packages/server-renderer/__tests__/renderToString.spec.ts
+++ b/packages/server-renderer/__tests__/renderToString.spec.ts
@@ -5,11 +5,16 @@ import {
withScopeId,
resolveComponent,
ComponentOptions,
+ Portal,
ref,
defineComponent
} from 'vue'
import { escapeHtml, mockWarn } from '@vue/shared'
-import { renderToString, renderComponent } from '../src/renderToString'
+import {
+ renderToString,
+ renderComponent,
+ SSRContext
+} from '../src/renderToString'
import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
mockWarn()
@@ -508,6 +513,21 @@ describe('ssr: renderToString', () => {
})
})
+ test('portal', async () => {
+ const ctx: SSRContext = {}
+ await renderToString(
+ h(
+ Portal,
+ {
+ target: `#target`
+ },
+ h('span', 'hello')
+ ),
+ ctx
+ )
+ expect(ctx.portals!['#target']).toBe('hello')
+ })
+
describe('scopeId', () => {
// note: here we are only testing scopeId handling for vdom serialization.
// compiled srr render functions will include scopeId directly in strings.
diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
index 3caaf4421e5..d8826a03e18 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
@@ -16,7 +16,7 @@ export function ssrRenderSlot(
slotProps: Props,
fallbackRenderFn: (() => void) | null,
push: PushFn,
- parentComponent: ComponentInternalInstance | null = null
+ parentComponent: ComponentInternalInstance
) {
const slotFn = slots[slotName]
// template-compiled slots are always rendered as fragments
diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts
index 599f6b34510..b7bd38d3135 100644
--- a/packages/server-renderer/src/renderToString.ts
+++ b/packages/server-renderer/src/renderToString.ts
@@ -11,7 +11,8 @@ import {
Portal,
ssrUtils,
Slots,
- warn
+ warn,
+ createApp
} from 'vue'
import {
ShapeFlags,
@@ -47,9 +48,22 @@ const {
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | ResolvedSSRBuffer | Promise
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
+
export type PushFn = (item: SSRBufferItem) => void
+
export type Props = Record
+const ssrContextKey = Symbol()
+
+export type SSRContext = {
+ [key: string]: any
+ portals?: Record
+ __portalBuffers?: Record<
+ string,
+ ResolvedSSRBuffer | Promise
+ >
+}
+
function createBuffer() {
let appendable = false
let hasAsync = false
@@ -88,17 +102,33 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
return ret
}
-export async function renderToString(input: App | VNode): Promise {
+export async function renderToString(
+ input: App | VNode,
+ context: SSRContext = {}
+): Promise {
let buffer: ResolvedSSRBuffer
if (isVNode(input)) {
- // raw vnode, wrap with component
- buffer = await renderComponent({ render: () => input })
+ // raw vnode, wrap with app (for context)
+ return renderToString(createApp({ render: () => input }), context)
} else {
// rendering an app
const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context
+ // provide the ssr context to the tree
+ input.provide(ssrContextKey, context)
buffer = await renderComponentVNode(vnode)
}
+
+ // resolve portals
+ if (context.__portalBuffers) {
+ context.portals = context.portals || {}
+ for (const key in context.__portalBuffers) {
+ // note: it's OK to await sequentially here because the Promises were
+ // created eagerly in parallel.
+ context.portals[key] = unrollBuffer(await context.__portalBuffers[key])
+ }
+ }
+
return unrollBuffer(buffer)
}
@@ -132,7 +162,7 @@ function renderComponentVNode(
}
type SSRRenderFunction = (
- ctx: any,
+ context: any,
push: (item: any) => void,
parentInstance: ComponentInternalInstance
) => void
@@ -206,7 +236,7 @@ function renderComponentSubTree(
function renderVNode(
push: PushFn,
vnode: VNode,
- parentComponent: ComponentInternalInstance | null = null
+ parentComponent: ComponentInternalInstance
) {
const { type, shapeFlag, children } = vnode
switch (type) {
@@ -222,7 +252,7 @@ function renderVNode(
push(``)
break
case Portal:
- // TODO
+ renderPortal(vnode, parentComponent)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
@@ -244,7 +274,7 @@ function renderVNode(
export function renderVNodeChildren(
push: PushFn,
children: VNodeArrayChildren,
- parentComponent: ComponentInternalInstance | null = null
+ parentComponent: ComponentInternalInstance
) {
for (let i = 0; i < children.length; i++) {
renderVNode(push, normalizeVNode(children[i]), parentComponent)
@@ -254,7 +284,7 @@ export function renderVNodeChildren(
function renderElement(
push: PushFn,
vnode: VNode,
- parentComponent: ComponentInternalInstance | null = null
+ parentComponent: ComponentInternalInstance
) {
const tag = vnode.type as string
const { props, children, shapeFlag, scopeId } = vnode
@@ -305,3 +335,35 @@ function renderElement(
push(`${tag}>`)
}
}
+
+function renderPortal(
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance
+) {
+ const target = vnode.props && vnode.props.target
+ if (!target) {
+ console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
+ return []
+ }
+ if (!isString(target)) {
+ console.warn(
+ `[@vue/server-renderer] Portal target must be a query selector string.`
+ )
+ return []
+ }
+
+ const { buffer, push, hasAsync } = createBuffer()
+ renderVNodeChildren(
+ push,
+ vnode.children as VNodeArrayChildren,
+ parentComponent
+ )
+ const context = parentComponent.appContext.provides[
+ ssrContextKey as any
+ ] as SSRContext
+ const portalBuffers =
+ context.__portalBuffers || (context.__portalBuffers = {})
+ portalBuffers[target] = hasAsync()
+ ? Promise.all(buffer)
+ : (buffer as ResolvedSSRBuffer)
+}