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

feat(ssr): render portals #714

Merged
merged 7 commits into from
Feb 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion packages/server-renderer/__tests__/renderToString.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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('<span>hello</span>')
})

describe('scopeId', () => {
// note: here we are only testing scopeId handling for vdom serialization.
// compiled srr render functions will include scopeId directly in strings.
Expand Down
2 changes: 1 addition & 1 deletion packages/server-renderer/src/helpers/ssrRenderSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 71 additions & 9 deletions packages/server-renderer/src/renderToString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
Portal,
ssrUtils,
Slots,
warn
warn,
createApp
} from 'vue'
import {
ShapeFlags,
Expand Down Expand Up @@ -47,9 +48,22 @@ const {
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]

export type PushFn = (item: SSRBufferItem) => void

export type Props = Record<string, unknown>

const ssrContextKey = Symbol()

export type SSRContext = {
[key: string]: any
portals?: Record<string, string>
__portalBuffers?: Record<
string,
ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
>
}

function createBuffer() {
let appendable = false
let hasAsync = false
Expand Down Expand Up @@ -88,17 +102,33 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
return ret
}

export async function renderToString(input: App | VNode): Promise<string> {
export async function renderToString(
input: App | VNode,
context: SSRContext = {}
): Promise<string> {
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)
}

Expand Down Expand Up @@ -132,7 +162,7 @@ function renderComponentVNode(
}

type SSRRenderFunction = (
ctx: any,
context: any,
push: (item: any) => void,
parentInstance: ComponentInternalInstance
) => void
Expand Down Expand Up @@ -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) {
Expand All @@ -222,7 +252,7 @@ function renderVNode(
push(`<!---->`)
break
case Portal:
// TODO
renderPortal(vnode, parentComponent)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
}